aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar bai 2019-03-29 02:14:43 +0000
committerLibravatar bai 2019-03-29 02:14:43 +0000
commit95dfe14528663923ca2a88ec928f1d8d9df2402b (patch)
tree5bc88d1466957f1aa39043b056bde5c439648022
downloadweabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.tar.gz
weabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.tar.xz
weabot-95dfe14528663923ca2a88ec928f1d8d9df2402b.zip
Init
-rw-r--r--cgi/.htaccess9
-rw-r--r--cgi/BeautifulSoup.py2017
-rw-r--r--cgi/GeoIP.datbin0 -> 878459 bytes
-rw-r--r--cgi/anarkia.py439
-rw-r--r--cgi/api.py392
-rw-r--r--cgi/database.py69
-rw-r--r--cgi/fcgi.py1332
-rw-r--r--cgi/formatting.py425
-rw-r--r--cgi/framework.py467
-rw-r--r--cgi/geoip.py128
-rw-r--r--cgi/img.py416
-rw-r--r--cgi/locale/es/LC_MESSAGES/weabot.mobin0 -> 17305 bytes
-rw-r--r--cgi/manage.py1823
-rw-r--r--cgi/markdown.py2044
-rw-r--r--cgi/oekaki.py176
-rw-r--r--cgi/post.py1260
-rw-r--r--cgi/proxy.txt3251
-rw-r--r--cgi/quotes.conf13
-rw-r--r--cgi/template.py117
-rw-r--r--cgi/templates/anarkia.html329
-rw-r--r--cgi/templates/banned.html34
-rw-r--r--cgi/templates/base_bottom.html3
-rw-r--r--cgi/templates/base_top.html55
-rw-r--r--cgi/templates/board.0.html230
-rw-r--r--cgi/templates/board.html264
-rw-r--r--cgi/templates/board.jp.html271
-rw-r--r--cgi/templates/catalog.html30
-rw-r--r--cgi/templates/error.html7
-rw-r--r--cgi/templates/exception.html36
-rw-r--r--cgi/templates/home.rss24
-rw-r--r--cgi/templates/htaccess24
-rw-r--r--cgi/templates/kako.html60
-rw-r--r--cgi/templates/manage/addboard.html21
-rw-r--r--cgi/templates/manage/bans.html92
-rw-r--r--cgi/templates/manage/boardoptions.html195
-rw-r--r--cgi/templates/manage/changepassword.html24
-rw-r--r--cgi/templates/manage/delete.html23
-rw-r--r--cgi/templates/manage/filters.html119
-rw-r--r--cgi/templates/manage/ipdelete.html24
-rw-r--r--cgi/templates/manage/ipshow.html73
-rw-r--r--cgi/templates/manage/lockboard.html20
-rw-r--r--cgi/templates/manage/login.html21
-rw-r--r--cgi/templates/manage/logs.html17
-rw-r--r--cgi/templates/manage/manage.html22
-rw-r--r--cgi/templates/manage/menu.html30
-rw-r--r--cgi/templates/manage/message.html8
-rw-r--r--cgi/templates/manage/mod.html96
-rw-r--r--cgi/templates/manage/move.html60
-rw-r--r--cgi/templates/manage/quotes.html12
-rw-r--r--cgi/templates/manage/rebuild.html20
-rw-r--r--cgi/templates/manage/recent_images.html24
-rw-r--r--cgi/templates/manage/recyclebin.html72
-rw-r--r--cgi/templates/manage/reports.html58
-rw-r--r--cgi/templates/manage/search.html27
-rw-r--r--cgi/templates/manage/staff.html63
-rw-r--r--cgi/templates/mobile/base_top.html14
-rw-r--r--cgi/templates/mobile/board.html55
-rw-r--r--cgi/templates/mobile/error.html6
-rw-r--r--cgi/templates/mobile/latest.html14
-rw-r--r--cgi/templates/mobile/newest.html14
-rw-r--r--cgi/templates/mobile/threadlist.html43
-rw-r--r--cgi/templates/mobile/txt_newthread.html35
-rw-r--r--cgi/templates/mobile/txt_thread.html74
-rw-r--r--cgi/templates/mobile/txt_threadlist.html26
-rw-r--r--cgi/templates/mod.html86
-rw-r--r--cgi/templates/navbar.html16
-rw-r--r--cgi/templates/paint.html79
-rw-r--r--cgi/templates/redirect.html12
-rw-r--r--cgi/templates/report.html29
-rw-r--r--cgi/templates/revision.html1
-rw-r--r--cgi/templates/stats.html163
-rw-r--r--cgi/templates/txt_archive.html104
-rw-r--r--cgi/templates/txt_base_top.html44
-rw-r--r--cgi/templates/txt_board.en.html137
-rw-r--r--cgi/templates/txt_board.html137
-rw-r--r--cgi/templates/txt_error.html50
-rw-r--r--cgi/templates/txt_thread.en.html105
-rw-r--r--cgi/templates/txt_thread.html101
-rw-r--r--cgi/templates/txt_threadlist.html67
-rw-r--r--cgi/tenjin.py2118
-rw-r--r--cgi/tor.txt1140
-rwxr-xr-xcgi/weabot.py1021
-rw-r--r--static/css/buri.css23
-rw-r--r--static/css/cyber.css39
-rw-r--r--static/css/dickgirl.css23
-rw-r--r--static/css/easymodo.css25
-rw-r--r--static/css/futaba.css22
-rw-r--r--static/css/guro.css24
-rw-r--r--static/css/ib.css72
-rw-r--r--static/css/img/0back.pngbin0 -> 148 bytes
-rw-r--r--static/css/img/0info.pngbin0 -> 284 bytes
-rw-r--r--static/css/img/0pc.pngbin0 -> 276 bytes
-rw-r--r--static/css/img/ba.gifbin0 -> 856 bytes
-rw-r--r--static/css/img/barra_dulce.pngbin0 -> 1266 bytes
-rw-r--r--static/css/img/bg_deportes.gifbin0 -> 3584 bytes
-rw-r--r--static/css/img/bg_madera.pngbin0 -> 4216 bytes
-rw-r--r--static/css/img/bg_oculto.gifbin0 -> 871 bytes
-rw-r--r--static/css/img/bgtb.gifbin0 -> 854 bytes
-rw-r--r--static/css/img/checked.pngbin0 -> 110 bytes
-rw-r--r--static/css/img/cyb.pngbin0 -> 4844 bytes
-rw-r--r--static/css/img/cyba.pngbin0 -> 201 bytes
-rw-r--r--static/css/img/fondo2012.gifbin0 -> 72620 bytes
-rw-r--r--static/css/img/green.gifbin0 -> 37408 bytes
-rw-r--r--static/css/img/hand.pngbin0 -> 213 bytes
-rw-r--r--static/css/img/luz.gifbin0 -> 3413 bytes
-rw-r--r--static/css/img/muro.jpgbin0 -> 37725 bytes
-rw-r--r--static/css/img/nieve.pngbin0 -> 2394 bytes
-rw-r--r--static/css/img/picnicbdy.gifbin0 -> 1049 bytes
-rw-r--r--static/css/img/picnicbg.gifbin0 -> 10945 bytes
-rw-r--r--static/css/img/picnicbtm.gifbin0 -> 3923 bytes
-rw-r--r--static/css/img/picnicbtn.gifbin0 -> 436 bytes
-rw-r--r--static/css/img/picnicfg.gifbin0 -> 1044 bytes
-rw-r--r--static/css/img/picnichr.gifbin0 -> 1368 bytes
-rw-r--r--static/css/img/picnicmid.gifbin0 -> 9502 bytes
-rw-r--r--static/css/img/picnicthr1.gifbin0 -> 12688 bytes
-rw-r--r--static/css/img/picnicthr2.gifbin0 -> 2946 bytes
-rw-r--r--static/css/img/picnicthr3.gifbin0 -> 12154 bytes
-rw-r--r--static/css/img/picnictop.gifbin0 -> 6939 bytes
-rw-r--r--static/css/img/scan.pngbin0 -> 182 bytes
-rw-r--r--static/css/img/scroller1.gifbin0 -> 3456 bytes
-rw-r--r--static/css/img/tanasinn.gifbin0 -> 1176 bytes
-rw-r--r--static/css/img/vndb1.jpgbin0 -> 36419 bytes
-rw-r--r--static/css/img/vndb2.jpgbin0 -> 4366 bytes
-rw-r--r--static/css/img/vndb3.pngbin0 -> 2791 bytes
-rw-r--r--static/css/kraut.css24
-rw-r--r--static/css/mobile.css129
-rw-r--r--static/css/night.css22
-rw-r--r--static/css/photon.css22
-rw-r--r--static/css/putaba.css46
-rw-r--r--static/css/red.css21
-rw-r--r--static/css/rene.css22
-rw-r--r--static/css/spc/base.css269
-rw-r--r--static/css/spc/halloween.css47
-rw-r--r--static/css/spc/layout.css58
-rw-r--r--static/css/spc/navidad.css161
-rw-r--r--static/css/spc/skeleton.css242
-rw-r--r--static/css/spc/valentin.css170
-rw-r--r--static/css/spc/valentin2.css177
-rw-r--r--static/css/txt/4am.css42
-rw-r--r--static/css/txt/amber.css44
-rw-r--r--static/css/txt/ayashii.css52
-rw-r--r--static/css/txt/baisano.css43
-rw-r--r--static/css/txt/bbs.css95
-rw-r--r--static/css/txt/bios.css51
-rw-r--r--static/css/txt/blue moon.css61
-rw-r--r--static/css/txt/ciber.css55
-rw-r--r--static/css/txt/futanari.css49
-rw-r--r--static/css/txt/headline.css41
-rw-r--r--static/css/txt/postal.css47
-rw-r--r--static/css/txt/sjis.css1
-rw-r--r--static/css/txt/ventanas.css48
-rw-r--r--static/css/vndb.css30
-rw-r--r--static/ico/1372836.gifbin0 -> 374 bytes
-rw-r--r--static/ico/1k.gifbin0 -> 101 bytes
-rw-r--r--static/ico/2-1.gifbin0 -> 1012 bytes
-rw-r--r--static/ico/2ppa.gifbin0 -> 285 bytes
-rw-r--r--static/ico/2syobo_2.gifbin0 -> 270 bytes
-rw-r--r--static/ico/3-2.gifbin0 -> 19802 bytes
-rw-r--r--static/ico/3.gifbin0 -> 1006 bytes
-rw-r--r--static/ico/3na.gifbin0 -> 324 bytes
-rw-r--r--static/ico/4-2.gifbin0 -> 1107 bytes
-rw-r--r--static/ico/4248688.gifbin0 -> 8947 bytes
-rw-r--r--static/ico/5007629.gifbin0 -> 14263 bytes
-rw-r--r--static/ico/5296219.gifbin0 -> 4028 bytes
-rw-r--r--static/ico/5ta.gifbin0 -> 364 bytes
-rw-r--r--static/ico/6396408.gifbin0 -> 8590 bytes
-rw-r--r--static/ico/6za.gifbin0 -> 395 bytes
-rw-r--r--static/ico/8028885.gifbin0 -> 41133 bytes
-rw-r--r--static/ico/8toushinnomonar16.gifbin0 -> 118 bytes
-rw-r--r--static/ico/8toushinnomonar32.gifbin0 -> 247 bytes
-rw-r--r--static/ico/ace.gifbin0 -> 317 bytes
-rw-r--r--static/ico/af1.gifbin0 -> 254 bytes
-rw-r--r--static/ico/af2.gifbin0 -> 228 bytes
-rw-r--r--static/ico/ahya_xmas_2.gifbin0 -> 227 bytes
-rw-r--r--static/ico/aka.gifbin0 -> 307 bytes
-rw-r--r--static/ico/ame.gifbin0 -> 224 bytes
-rw-r--r--static/ico/anime_buun02.gifbin0 -> 2036 bytes
-rw-r--r--static/ico/anime_charhan01.gifbin0 -> 2257 bytes
-rw-r--r--static/ico/anime_charhan02.gifbin0 -> 10274 bytes
-rw-r--r--static/ico/anime_giko01.gifbin0 -> 2183 bytes
-rw-r--r--static/ico/anime_giko04.gifbin0 -> 1516 bytes
-rw-r--r--static/ico/anime_giko10.gifbin0 -> 2250 bytes
-rw-r--r--static/ico/anime_giko11.gifbin0 -> 2291 bytes
-rw-r--r--static/ico/anime_giko12.gifbin0 -> 1760 bytes
-rw-r--r--static/ico/anime_giko13.gifbin0 -> 3135 bytes
-rw-r--r--static/ico/anime_hossyu01.gifbin0 -> 1300 bytes
-rw-r--r--static/ico/anime_imanouchi01.gifbin0 -> 1459 bytes
-rw-r--r--static/ico/anime_iyou02.gifbin0 -> 3351 bytes
-rw-r--r--static/ico/anime_jien01.gifbin0 -> 3345 bytes
-rw-r--r--static/ico/anime_jien02.gifbin0 -> 3403 bytes
-rw-r--r--static/ico/anime_jien03.gifbin0 -> 3231 bytes
-rw-r--r--static/ico/anime_jyorujyu01.gifbin0 -> 1431 bytes
-rw-r--r--static/ico/anime_jyorujyu02.gifbin0 -> 1441 bytes
-rw-r--r--static/ico/anime_jyorujyu03.gifbin0 -> 1450 bytes
-rw-r--r--static/ico/anime_kukkuru01.gifbin0 -> 1665 bytes
-rw-r--r--static/ico/anime_kuma01.gifbin0 -> 2099 bytes
-rw-r--r--static/ico/anime_kumaface01.gifbin0 -> 3924 bytes
-rw-r--r--static/ico/anime_loop.gifbin0 -> 20749 bytes
-rw-r--r--static/ico/anime_marara02.gifbin0 -> 1925 bytes
-rw-r--r--static/ico/anime_matanki01.gifbin0 -> 1392 bytes
-rw-r--r--static/ico/anime_matanki02.gifbin0 -> 4616 bytes
-rw-r--r--static/ico/anime_miruna01.gifbin0 -> 1567 bytes
-rw-r--r--static/ico/anime_monar02.gifbin0 -> 1594 bytes
-rw-r--r--static/ico/anime_monar03.gifbin0 -> 2564 bytes
-rw-r--r--static/ico/anime_monar05.gifbin0 -> 5846 bytes
-rw-r--r--static/ico/anime_morara01.gifbin0 -> 1576 bytes
-rw-r--r--static/ico/anime_morara02.gifbin0 -> 1734 bytes
-rw-r--r--static/ico/anime_morara04.gifbin0 -> 1634 bytes
-rw-r--r--static/ico/anime_nokar01.gifbin0 -> 2334 bytes
-rw-r--r--static/ico/anime_okashi01.gifbin0 -> 5999 bytes
-rw-r--r--static/ico/anime_okashi02.gifbin0 -> 4348 bytes
-rw-r--r--static/ico/anime_onigiri04.gifbin0 -> 8654 bytes
-rw-r--r--static/ico/anime_saitama01.gifbin0 -> 1440 bytes
-rw-r--r--static/ico/anime_saitama02.gifbin0 -> 1348 bytes
-rw-r--r--static/ico/anime_saitama03.gifbin0 -> 1454 bytes
-rw-r--r--static/ico/anime_sasuga01.gifbin0 -> 1546 bytes
-rw-r--r--static/ico/anime_sasuga03.gifbin0 -> 9729 bytes
-rw-r--r--static/ico/anime_sasuga04.gifbin0 -> 16392 bytes
-rw-r--r--static/ico/anime_shii01.gifbin0 -> 11664 bytes
-rw-r--r--static/ico/anime_shii02.gifbin0 -> 8324 bytes
-rw-r--r--static/ico/anime_shii03.gifbin0 -> 1506 bytes
-rw-r--r--static/ico/anime_syobon01.gifbin0 -> 2156 bytes
-rw-r--r--static/ico/anime_syobon03.gifbin0 -> 2361 bytes
-rw-r--r--static/ico/anime_tarn01.gifbin0 -> 3754 bytes
-rw-r--r--static/ico/anime_uwan01.gifbin0 -> 1966 bytes
-rw-r--r--static/ico/anime_uwan02.gifbin0 -> 1441 bytes
-rw-r--r--static/ico/anime_uwan03.gifbin0 -> 2216 bytes
-rw-r--r--static/ico/anime_youkanman01.gifbin0 -> 1915 bytes
-rw-r--r--static/ico/anime_youkanman02.gifbin0 -> 1882 bytes
-rw-r--r--static/ico/anime_youkanman03.gifbin0 -> 1365 bytes
-rw-r--r--static/ico/anime_zonu01.gifbin0 -> 1426 bytes
-rw-r--r--static/ico/anime_zonu02.gifbin0 -> 1476 bytes
-rw-r--r--static/ico/aramaki.gifbin0 -> 236 bytes
-rw-r--r--static/ico/aroeri-na32.gifbin0 -> 268 bytes
-rw-r--r--static/ico/asopasomaso.gifbin0 -> 247 bytes
-rw-r--r--static/ico/bikyakusan32.gifbin0 -> 213 bytes
-rw-r--r--static/ico/bs.gifbin0 -> 378 bytes
-rw-r--r--static/ico/button1_03.gifbin0 -> 218 bytes
-rw-r--r--static/ico/buun.gifbin0 -> 251 bytes
-rw-r--r--static/ico/chahan.gifbin0 -> 279 bytes
-rw-r--r--static/ico/dokuo1.gifbin0 -> 414 bytes
-rw-r--r--static/ico/file2_01.gifbin0 -> 282 bytes
-rw-r--r--static/ico/fujisan.gifbin0 -> 216 bytes
-rw-r--r--static/ico/fuun.gifbin0 -> 244 bytes
-rw-r--r--static/ico/gaku.gifbin0 -> 196 bytes
-rw-r--r--static/ico/gaku2.gifbin0 -> 252 bytes
-rw-r--r--static/ico/gaku3.gifbin0 -> 199 bytes
-rw-r--r--static/ico/gekisya1.gifbin0 -> 333 bytes
-rw-r--r--static/ico/giko1.gifbin0 -> 407 bytes
-rw-r--r--static/ico/gikog_gomibako.gifbin0 -> 233 bytes
-rw-r--r--static/ico/gikog_gyunyupack.gifbin0 -> 277 bytes
-rw-r--r--static/ico/gikog_pimiento.gifbin0 -> 279 bytes
-rw-r--r--static/ico/gikoinu.gifbin0 -> 252 bytes
-rw-r--r--static/ico/gikoneko.gifbin0 -> 997 bytes
-rw-r--r--static/ico/gikoneko2.gifbin0 -> 1021 bytes
-rw-r--r--static/ico/gikoneko_1.gifbin0 -> 200 bytes
-rw-r--r--static/ico/gocchin_face.gifbin0 -> 256 bytes
-rw-r--r--static/ico/gomiopen.gifbin0 -> 425 bytes
-rw-r--r--static/ico/goo_1.gifbin0 -> 209 bytes
-rw-r--r--static/ico/goo_3.gifbin0 -> 213 bytes
-rw-r--r--static/ico/gya-.gifbin0 -> 129 bytes
-rw-r--r--static/ico/hagenin-shuriken.gifbin0 -> 8609 bytes
-rw-r--r--static/ico/hagurumaou.gifbin0 -> 283 bytes
-rw-r--r--static/ico/hikky.gifbin0 -> 246 bytes
-rw-r--r--static/ico/hikky_xmas_2.gifbin0 -> 250 bytes
-rw-r--r--static/ico/hyou.gifbin0 -> 212 bytes
-rw-r--r--static/ico/iirasan_face.gifbin0 -> 283 bytes
-rw-r--r--static/ico/imanouchi_1.gifbin0 -> 232 bytes
-rw-r--r--static/ico/iyahoo.gifbin0 -> 1456 bytes
-rw-r--r--static/ico/iyou.gifbin0 -> 233 bytes
-rw-r--r--static/ico/jisakujien_2.gifbin0 -> 181 bytes
-rw-r--r--static/ico/jisakujien_xmas.gifbin0 -> 219 bytes
-rw-r--r--static/ico/kantoku1.gifbin0 -> 314 bytes
-rw-r--r--static/ico/kappappa1.gifbin0 -> 416 bytes
-rw-r--r--static/ico/kasa-ri.gifbin0 -> 196 bytes
-rw-r--r--static/ico/kashiwamo-chi32.gifbin0 -> 352 bytes
-rw-r--r--static/ico/kinokorusensei32.gifbin0 -> 337 bytes
-rw-r--r--static/ico/kita_.gifbin0 -> 260 bytes
-rw-r--r--static/ico/kodomona.gifbin0 -> 237 bytes
-rw-r--r--static/ico/konkon_folder.gifbin0 -> 373 bytes
-rw-r--r--static/ico/kossorisan.gifbin0 -> 99 bytes
-rw-r--r--static/ico/kotatu.gifbin0 -> 248 bytes
-rw-r--r--static/ico/kuma.gifbin0 -> 3875 bytes
-rw-r--r--static/ico/kuma2.gifbin0 -> 484 bytes
-rw-r--r--static/ico/maimai.gifbin0 -> 327 bytes
-rw-r--r--static/ico/makotan2_folder.gifbin0 -> 389 bytes
-rw-r--r--static/ico/mona.gifbin0 -> 254 bytes
-rw-r--r--static/ico/mona_shiri.gifbin0 -> 238 bytes
-rw-r--r--static/ico/mona_tya.gifbin0 -> 298 bytes
-rw-r--r--static/ico/monaazarashi_1.gifbin0 -> 168 bytes
-rw-r--r--static/ico/namaetukenai.gifbin0 -> 3331 bytes
-rw-r--r--static/ico/naoruyo.gifbin0 -> 257 bytes
-rw-r--r--static/ico/nida.gifbin0 -> 254 bytes
-rw-r--r--static/ico/nigete.gifbin0 -> 2304 bytes
-rw-r--r--static/ico/nono_ie.gifbin0 -> 365 bytes
-rw-r--r--static/ico/nurupo_ga_2.gifbin0 -> 1270 bytes
-rw-r--r--static/ico/onigiri_seito.gifbin0 -> 288 bytes
-rw-r--r--static/ico/otiketu48.gifbin0 -> 378 bytes
-rw-r--r--static/ico/pc3.gifbin0 -> 288 bytes
-rw-r--r--static/ico/pgya.gifbin0 -> 288 bytes
-rw-r--r--static/ico/sasuga1.gifbin0 -> 15680 bytes
-rw-r--r--static/ico/seito_2.gifbin0 -> 216 bytes
-rw-r--r--static/ico/soon.gifbin0 -> 245 bytes
-rw-r--r--static/ico/tasukete.gifbin0 -> 312 bytes
-rw-r--r--static/ico/torimasu1.gifbin0 -> 317 bytes
-rw-r--r--static/ico/torimasu2.gifbin0 -> 313 bytes
-rw-r--r--static/ico/u_ame.gifbin0 -> 3608 bytes
-rw-r--r--static/ico/u_hoshi.gifbin0 -> 3076 bytes
-rw-r--r--static/ico/u_naoruyo_bath.gifbin0 -> 4552 bytes
-rw-r--r--static/ico/u_okotowari_a.gifbin0 -> 6831 bytes
-rw-r--r--static/ico/u_sofa.gifbin0 -> 1280 bytes
-rw-r--r--static/ico/wakannai1.gifbin0 -> 304 bytes
-rw-r--r--static/ico/yakimochi.gifbin0 -> 242 bytes
-rw-r--r--static/ico/youkan.gifbin0 -> 254 bytes
-rw-r--r--static/ico/zonu_1.gifbin0 -> 179 bytes
-rw-r--r--static/ico/zuzagiko48.gifbin0 -> 368 bytes
-rw-r--r--static/img/anarkia.jpgbin0 -> 35677 bytes
-rw-r--r--static/img/bai.jpgbin0 -> 38030 bytes
-rw-r--r--static/img/cero.gifbin0 -> 80262 bytes
-rw-r--r--static/img/default.pngbin0 -> 9073 bytes
-rw-r--r--static/img/juegos1.jpgbin0 -> 43984 bytes
-rw-r--r--static/img/juegos2.jpgbin0 -> 35649 bytes
-rw-r--r--static/img/juegos3.pngbin0 -> 10301 bytes
-rw-r--r--static/img/juegos4.gifbin0 -> 24664 bytes
-rw-r--r--static/img/letras1.pngbin0 -> 52461 bytes
-rw-r--r--static/img/letras2.pngbin0 -> 69712 bytes
-rw-r--r--static/img/letras3.pngbin0 -> 97055 bytes
-rw-r--r--static/img/letras4.jpgbin0 -> 46084 bytes
-rw-r--r--static/img/letras5.jpgbin0 -> 50947 bytes
-rw-r--r--static/img/musica1.jpgbin0 -> 24487 bytes
-rw-r--r--static/img/noticias.pngbin0 -> 13249 bytes
-rw-r--r--static/img/old/2d_1.jpgbin0 -> 29384 bytes
-rw-r--r--static/img/old/2d_2.jpgbin0 -> 36070 bytes
-rw-r--r--static/img/old/2d_3.pngbin0 -> 5899 bytes
-rw-r--r--static/img/old/2d_4.jpgbin0 -> 29392 bytes
-rw-r--r--static/img/old/argentina1.pngbin0 -> 27794 bytes
-rw-r--r--static/img/old/chile1.pngbin0 -> 44923 bytes
-rw-r--r--static/img/old/chile2.jpgbin0 -> 44202 bytes
-rw-r--r--static/img/old/g0.jpgbin0 -> 39681 bytes
-rw-r--r--static/img/old/g1.jpgbin0 -> 45641 bytes
-rw-r--r--static/img/old/g2.jpgbin0 -> 38041 bytes
-rw-r--r--static/img/old/g3.jpgbin0 -> 52934 bytes
-rw-r--r--static/img/old/g4.jpgbin0 -> 36197 bytes
-rw-r--r--static/img/old/g5.jpgbin0 -> 23641 bytes
-rw-r--r--static/img/old/peli.jpgbin0 -> 36293 bytes
-rw-r--r--static/img/old/salon2d_3.jpgbin0 -> 35496 bytes
-rw-r--r--static/img/old/salon2d_4.pngbin0 -> 4978 bytes
-rw-r--r--static/img/old/salon2d_5.jpgbin0 -> 25550 bytes
-rw-r--r--static/img/old/zine.pngbin0 -> 75774 bytes
-rw-r--r--static/img/salon2d_1.pngbin0 -> 15409 bytes
-rw-r--r--static/img/salon2d_3.jpgbin0 -> 35496 bytes
-rw-r--r--static/img/salon2d_4.pngbin0 -> 4978 bytes
-rw-r--r--static/img/salon2d_5.jpgbin0 -> 25550 bytes
-rw-r--r--static/img/tech1.pngbin0 -> 7802 bytes
-rw-r--r--static/img/tech2.jpgbin0 -> 36871 bytes
-rw-r--r--static/img/tech3.pngbin0 -> 42940 bytes
-rw-r--r--static/img/tech4.jpgbin0 -> 48759 bytes
-rw-r--r--static/img/tech5.jpgbin0 -> 37524 bytes
-rw-r--r--static/img/tech6.pngbin0 -> 17938 bytes
-rw-r--r--static/img/tv1.pngbin0 -> 3501 bytes
-rw-r--r--static/img/weird-al.jpgbin0 -> 14523 bytes
-rw-r--r--static/img/world.gifbin0 -> 100563 bytes
-rw-r--r--static/img/zonavip1.jpgbin0 -> 45584 bytes
-rw-r--r--static/img/zonavip2.gifbin0 -> 38269 bytes
-rw-r--r--static/img/zonavip3.pngbin0 -> 85601 bytes
-rw-r--r--static/img/zonavip4.jpgbin0 -> 41923 bytes
-rw-r--r--static/img/zonavip5.gifbin0 -> 35591 bytes
-rw-r--r--static/img/zonavip6.pngbin0 -> 13149 bytes
-rw-r--r--static/img/zonavip7.gifbin0 -> 50028 bytes
-rw-r--r--static/img/zonavip8.pngbin0 -> 53463 bytes
-rw-r--r--static/img/zonavip9.gifbin0 -> 19603 bytes
-rw-r--r--static/img/zonavip9.jpgbin0 -> 46773 bytes
-rw-r--r--static/img/zonavip_halloween.jpgbin0 -> 38379 bytes
-rw-r--r--static/img/zonavip_nav.jpgbin0 -> 51171 bytes
-rw-r--r--static/js/aquiencitas.js168
-rw-r--r--static/js/autorefresh.js275
-rw-r--r--static/js/home.js173
-rw-r--r--static/js/jquery.js545
-rw-r--r--static/js/manage.js22
-rw-r--r--static/js/mobile.js447
-rw-r--r--static/js/paintbbs/PaintBBS-1.1.11.css535
-rw-r--r--static/js/paintbbs/PaintBBS-1.1.11.js5686
-rw-r--r--static/js/paintbbs/PaintBBS-1.3.4.css547
-rw-r--r--static/js/paintbbs/PaintBBS-1.3.4.js6171
-rwxr-xr-xstatic/js/palette_selfy.js972
-rw-r--r--static/js/shobon.js408
-rw-r--r--static/js/tegaki/tegaki.css187
-rw-r--r--static/js/tegaki/tegaki.js1947
-rw-r--r--static/js/weabot.js456
-rw-r--r--static/js/weabotxt.js299
-rw-r--r--static/js/wpaint/.gitignore3
-rw-r--r--static/js/wpaint/README.md421
-rw-r--r--static/js/wpaint/bai.js23
-rw-r--r--static/js/wpaint/demo/demo.css266
-rw-r--r--static/js/wpaint/demo/img/facebook-icon.pngbin0 -> 274 bytes
-rw-r--r--static/js/wpaint/demo/img/favicon.icobin0 -> 1150 bytes
-rw-r--r--static/js/wpaint/demo/img/forkme_right_darkblue.pngbin0 -> 7791 bytes
-rw-r--r--static/js/wpaint/demo/img/github-icon.pngbin0 -> 596 bytes
-rw-r--r--static/js/wpaint/demo/img/googleplus-icon.pngbin0 -> 522 bytes
-rw-r--r--static/js/wpaint/demo/img/linkedin-icon.pngbin0 -> 380 bytes
-rw-r--r--static/js/wpaint/demo/img/rss-icon.pngbin0 -> 521 bytes
-rw-r--r--static/js/wpaint/demo/img/stumbleupon-icon.pngbin0 -> 537 bytes
-rw-r--r--static/js/wpaint/demo/img/twitter-icon.pngbin0 -> 514 bytes
-rw-r--r--static/js/wpaint/demo/img/websanova-logo-small-full-black.pngbin0 -> 1028 bytes
-rw-r--r--static/js/wpaint/demo/img/youtube-icon.pngbin0 -> 587 bytes
-rw-r--r--static/js/wpaint/gruntfile.js90
-rw-r--r--static/js/wpaint/index.html136
-rw-r--r--static/js/wpaint/lib/jquery.1.10.2.min.js6
-rw-r--r--static/js/wpaint/lib/jquery.ui.core.1.10.3.min.js4
-rw-r--r--static/js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js4
-rw-r--r--static/js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js4
-rw-r--r--static/js/wpaint/lib/jquery.ui.widget.1.10.3.min.js4
-rw-r--r--static/js/wpaint/lib/mixins.styl7
-rw-r--r--static/js/wpaint/lib/wColorPicker.min.css42
-rw-r--r--static/js/wpaint/lib/wColorPicker.min.js2
-rw-r--r--static/js/wpaint/package.json25
-rw-r--r--static/js/wpaint/plugins/file/img/icons-menu-main-file.pngbin0 -> 835 bytes
-rw-r--r--static/js/wpaint/plugins/file/src/wPaint.menu.main.file.js75
-rw-r--r--static/js/wpaint/plugins/file/wPaint.menu.main.file.min.js1
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-bucket.pngbin0 -> 450 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-crosshair.pngbin0 -> 208 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-dropper.pngbin0 -> 403 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser1.pngbin0 -> 193 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser10.pngbin0 -> 247 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser2.pngbin0 -> 200 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser3.pngbin0 -> 206 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser4.pngbin0 -> 209 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser5.pngbin0 -> 225 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser6.pngbin0 -> 229 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser7.pngbin0 -> 236 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser8.pngbin0 -> 240 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-eraser9.pngbin0 -> 244 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/cursor-pencil.pngbin0 -> 449 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/icon-group-arrow.pngbin0 -> 208 bytes
-rw-r--r--static/js/wpaint/plugins/main/img/icons-menu-main.pngbin0 -> 2836 bytes
-rw-r--r--static/js/wpaint/plugins/main/src/fillArea.min.js1
-rw-r--r--static/js/wpaint/plugins/main/src/wPaint.menu.main.js338
-rw-r--r--static/js/wpaint/plugins/main/wPaint.menu.main.min.js1
-rw-r--r--static/js/wpaint/plugins/shapes/img/icons-menu-main-shapes.pngbin0 -> 903 bytes
-rw-r--r--static/js/wpaint/plugins/shapes/src/shapes.min.js1
-rw-r--r--static/js/wpaint/plugins/shapes/src/wPaint.menu.main.shapes.js207
-rw-r--r--static/js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js1
-rw-r--r--static/js/wpaint/plugins/text/img/icons-menu-text.pngbin0 -> 802 bytes
-rw-r--r--static/js/wpaint/plugins/text/src/wPaint.menu.text.js227
-rw-r--r--static/js/wpaint/plugins/text/wPaint.menu.text.min.js1
-rw-r--r--static/js/wpaint/src/wPaint.css348
-rw-r--r--static/js/wpaint/src/wPaint.js1181
-rw-r--r--static/js/wpaint/src/wPaint.utils.js70
-rw-r--r--static/js/wpaint/test/dev.html123
-rw-r--r--static/js/wpaint/test/fullscreen.html79
-rw-r--r--static/js/wpaint/test/upload.php11
-rw-r--r--static/js/wpaint/test/uploads/test1.pngbin0 -> 432 bytes
-rw-r--r--static/js/wpaint/test/uploads/test2.pngbin0 -> 462 bytes
-rw-r--r--static/js/wpaint/test/uploads/test3.pngbin0 -> 454 bytes
-rw-r--r--static/js/wpaint/test/uploads/wPaint.pngbin0 -> 3096 bytes
-rw-r--r--static/js/wpaint/wPaint.jquery.json38
-rw-r--r--static/js/wpaint/wPaint.min.css66
-rw-r--r--static/js/wpaint/wPaint.min.js1
-rw-r--r--static/meta/bbs.pngbin0 -> 7192 bytes
-rw-r--r--static/meta/faq_1.pngbin0 -> 18358 bytes
-rw-r--r--static/meta/faq_2.pngbin0 -> 12504 bytes
-rw-r--r--static/meta/faq_3.pngbin0 -> 19818 bytes
-rw-r--r--static/meta/ib.pngbin0 -> 10709 bytes
-rw-r--r--static/meta/portada_6.jpgbin0 -> 368440 bytes
-rw-r--r--static/meta/portada_7.jpgbin0 -> 51433 bytes
-rw-r--r--static/meta/portada_8.jpgbin0 -> 266858 bytes
-rw-r--r--static/meta/portada_asp.gifbin0 -> 441933 bytes
-rw-r--r--static/meta/portada_cap1.jpgbin0 -> 101119 bytes
-rw-r--r--static/meta/portada_orig5.jpgbin0 -> 67175 bytes
-rw-r--r--static/meta/portada_toesca.jpgbin0 -> 35123 bytes
-rw-r--r--static/meta/portadaphil.jpgbin0 -> 102067 bytes
-rw-r--r--static/meta/primeraportada.pngbin0 -> 385869 bytes
-rw-r--r--static/meta/sanvalentin2013.jpgbin0 -> 98509 bytes
-rw-r--r--static/meta/welcome.gifbin0 -> 238888 bytes
-rw-r--r--static/meta/welcome.jpgbin0 -> 28985 bytes
-rw-r--r--static/mime/epub.pngbin0 -> 1320 bytes
-rw-r--r--static/mime/epub_small.pngbin0 -> 1019 bytes
-rw-r--r--static/mime/mod.pngbin0 -> 1362 bytes
-rw-r--r--static/mime/mod_small.pngbin0 -> 1058 bytes
-rw-r--r--static/mime/pdf.pngbin0 -> 1264 bytes
-rw-r--r--static/mime/pdf_small.pngbin0 -> 984 bytes
-rw-r--r--static/mime/s3m.pngbin0 -> 1397 bytes
-rw-r--r--static/mime/s3m_small.pngbin0 -> 1104 bytes
-rw-r--r--static/mime/swf.pngbin0 -> 1375 bytes
-rw-r--r--static/mime/swf_small.pngbin0 -> 1067 bytes
-rw-r--r--static/mime/torrent.pngbin0 -> 1377 bytes
-rw-r--r--static/mime/torrent_small.pngbin0 -> 1070 bytes
-rw-r--r--static/mime/xm.pngbin0 -> 1318 bytes
-rw-r--r--static/mime/xm_small.pngbin0 -> 1011 bytes
489 files changed, 47499 insertions, 0 deletions
diff --git a/cgi/.htaccess b/cgi/.htaccess
new file mode 100644
index 0000000..97a4f17
--- /dev/null
+++ b/cgi/.htaccess
@@ -0,0 +1,9 @@
+AddHandler cgi-script .py
+Options +ExecCGI
+
+# Uncomment if you want pretty URL (ie cgi/post)
+#RewriteEngine On
+#RewriteBase /cgi/
+#RewriteRule ^weabot\.py/ - [L]
+#RewriteRule ^(.*)$ weabot.py/$1 [L]
+
diff --git a/cgi/BeautifulSoup.py b/cgi/BeautifulSoup.py
new file mode 100644
index 0000000..7278215
--- /dev/null
+++ b/cgi/BeautifulSoup.py
@@ -0,0 +1,2017 @@
+"""Beautiful Soup
+Elixir and Tonic
+"The Screen-Scraper's Friend"
+http://www.crummy.com/software/BeautifulSoup/
+
+Beautiful Soup parses a (possibly invalid) XML or HTML document into a
+tree representation. It provides methods and Pythonic idioms that make
+it easy to navigate, search, and modify the tree.
+
+A well-formed XML/HTML document yields a well-formed data
+structure. An ill-formed XML/HTML document yields a correspondingly
+ill-formed data structure. If your document is only locally
+well-formed, you can use this library to find and process the
+well-formed part of it.
+
+Beautiful Soup works with Python 2.2 and up. It has no external
+dependencies, but you'll have more success at converting data to UTF-8
+if you also install these three packages:
+
+* chardet, for auto-detecting character encodings
+ http://chardet.feedparser.org/
+* cjkcodecs and iconv_codec, which add more encodings to the ones supported
+ by stock Python.
+ http://cjkpython.i18n.org/
+
+Beautiful Soup defines classes for two main parsing strategies:
+
+ * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific
+ language that kind of looks like XML.
+
+ * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid
+ or invalid. This class has web browser-like heuristics for
+ obtaining a sensible parse tree in the face of common HTML errors.
+
+Beautiful Soup also defines a class (UnicodeDammit) for autodetecting
+the encoding of an HTML or XML document, and converting it to
+Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser.
+
+For more than you ever wanted to know about Beautiful Soup, see the
+documentation:
+http://www.crummy.com/software/BeautifulSoup/documentation.html
+
+Here, have some legalese:
+
+Copyright (c) 2004-2010, Leonard Richardson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the the Beautiful Soup Consortium and All
+ Night Kosher Bakery nor the names of its contributors may be
+ used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT.
+
+"""
+from __future__ import generators
+
+__author__ = "Leonard Richardson (leonardr@segfault.org)"
+__version__ = "3.2.1"
+__copyright__ = "Copyright (c) 2004-2012 Leonard Richardson"
+__license__ = "New-style BSD"
+
+from sgmllib import SGMLParser, SGMLParseError
+import codecs
+import markupbase
+import types
+import re
+import sgmllib
+try:
+ from htmlentitydefs import name2codepoint
+except ImportError:
+ name2codepoint = {}
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+#These hacks make Beautiful Soup able to parse XML with namespaces
+sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
+markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match
+
+DEFAULT_OUTPUT_ENCODING = "utf-8"
+
+def _match_css_class(str):
+ """Build a RE to match the given CSS class."""
+ return re.compile(r"(^|.*\s)%s($|\s)" % str)
+
+# First, the classes that represent markup elements.
+
+class PageElement(object):
+ """Contains the navigational information for some part of the page
+ (either a tag or a piece of text)"""
+
+ def _invert(h):
+ "Cheap function to invert a hash."
+ i = {}
+ for k,v in h.items():
+ i[v] = k
+ return i
+
+ XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'",
+ "quot" : '"',
+ "amp" : "&",
+ "lt" : "<",
+ "gt" : ">" }
+
+ XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS)
+
+ def setup(self, parent=None, previous=None):
+ """Sets up the initial relations between this element and
+ other elements."""
+ self.parent = parent
+ self.previous = previous
+ self.next = None
+ self.previousSibling = None
+ self.nextSibling = None
+ if self.parent and self.parent.contents:
+ self.previousSibling = self.parent.contents[-1]
+ self.previousSibling.nextSibling = self
+
+ def replaceWith(self, replaceWith):
+ oldParent = self.parent
+ myIndex = self.parent.index(self)
+ if hasattr(replaceWith, "parent")\
+ and replaceWith.parent is self.parent:
+ # We're replacing this element with one of its siblings.
+ index = replaceWith.parent.index(replaceWith)
+ if index and index < myIndex:
+ # Furthermore, it comes before this element. That
+ # means that when we extract it, the index of this
+ # element will change.
+ myIndex = myIndex - 1
+ self.extract()
+ oldParent.insert(myIndex, replaceWith)
+
+ def replaceWithChildren(self):
+ myParent = self.parent
+ myIndex = self.parent.index(self)
+ self.extract()
+ reversedChildren = list(self.contents)
+ reversedChildren.reverse()
+ for child in reversedChildren:
+ myParent.insert(myIndex, child)
+
+ def extract(self):
+ """Destructively rips this element out of the tree."""
+ if self.parent:
+ try:
+ del self.parent.contents[self.parent.index(self)]
+ except ValueError:
+ pass
+
+ #Find the two elements that would be next to each other if
+ #this element (and any children) hadn't been parsed. Connect
+ #the two.
+ lastChild = self._lastRecursiveChild()
+ nextElement = lastChild.next
+
+ if self.previous:
+ self.previous.next = nextElement
+ if nextElement:
+ nextElement.previous = self.previous
+ self.previous = None
+ lastChild.next = None
+
+ self.parent = None
+ if self.previousSibling:
+ self.previousSibling.nextSibling = self.nextSibling
+ if self.nextSibling:
+ self.nextSibling.previousSibling = self.previousSibling
+ self.previousSibling = self.nextSibling = None
+ return self
+
+ def _lastRecursiveChild(self):
+ "Finds the last element beneath this object to be parsed."
+ lastChild = self
+ while hasattr(lastChild, 'contents') and lastChild.contents:
+ lastChild = lastChild.contents[-1]
+ return lastChild
+
+ def insert(self, position, newChild):
+ if isinstance(newChild, basestring) \
+ and not isinstance(newChild, NavigableString):
+ newChild = NavigableString(newChild)
+
+ position = min(position, len(self.contents))
+ if hasattr(newChild, 'parent') and newChild.parent is not None:
+ # We're 'inserting' an element that's already one
+ # of this object's children.
+ if newChild.parent is self:
+ index = self.index(newChild)
+ if index > position:
+ # Furthermore we're moving it further down the
+ # list of this object's children. That means that
+ # when we extract this element, our target index
+ # will jump down one.
+ position = position - 1
+ newChild.extract()
+
+ newChild.parent = self
+ previousChild = None
+ if position == 0:
+ newChild.previousSibling = None
+ newChild.previous = self
+ else:
+ previousChild = self.contents[position-1]
+ newChild.previousSibling = previousChild
+ newChild.previousSibling.nextSibling = newChild
+ newChild.previous = previousChild._lastRecursiveChild()
+ if newChild.previous:
+ newChild.previous.next = newChild
+
+ newChildsLastElement = newChild._lastRecursiveChild()
+
+ if position >= len(self.contents):
+ newChild.nextSibling = None
+
+ parent = self
+ parentsNextSibling = None
+ while not parentsNextSibling:
+ parentsNextSibling = parent.nextSibling
+ parent = parent.parent
+ if not parent: # This is the last element in the document.
+ break
+ if parentsNextSibling:
+ newChildsLastElement.next = parentsNextSibling
+ else:
+ newChildsLastElement.next = None
+ else:
+ nextChild = self.contents[position]
+ newChild.nextSibling = nextChild
+ if newChild.nextSibling:
+ newChild.nextSibling.previousSibling = newChild
+ newChildsLastElement.next = nextChild
+
+ if newChildsLastElement.next:
+ newChildsLastElement.next.previous = newChildsLastElement
+ self.contents.insert(position, newChild)
+
+ def append(self, tag):
+ """Appends the given tag to the contents of this tag."""
+ self.insert(len(self.contents), tag)
+
+ def findNext(self, name=None, attrs={}, text=None, **kwargs):
+ """Returns the first item that matches the given criteria and
+ appears after this Tag in the document."""
+ return self._findOne(self.findAllNext, name, attrs, text, **kwargs)
+
+ def findAllNext(self, name=None, attrs={}, text=None, limit=None,
+ **kwargs):
+ """Returns all items that match the given criteria and appear
+ after this Tag in the document."""
+ return self._findAll(name, attrs, text, limit, self.nextGenerator,
+ **kwargs)
+
+ def findNextSibling(self, name=None, attrs={}, text=None, **kwargs):
+ """Returns the closest sibling to this Tag that matches the
+ given criteria and appears after this Tag in the document."""
+ return self._findOne(self.findNextSiblings, name, attrs, text,
+ **kwargs)
+
+ def findNextSiblings(self, name=None, attrs={}, text=None, limit=None,
+ **kwargs):
+ """Returns the siblings of this Tag that match the given
+ criteria and appear after this Tag in the document."""
+ return self._findAll(name, attrs, text, limit,
+ self.nextSiblingGenerator, **kwargs)
+ fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x
+
+ def findPrevious(self, name=None, attrs={}, text=None, **kwargs):
+ """Returns the first item that matches the given criteria and
+ appears before this Tag in the document."""
+ return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs)
+
+ def findAllPrevious(self, name=None, attrs={}, text=None, limit=None,
+ **kwargs):
+ """Returns all items that match the given criteria and appear
+ before this Tag in the document."""
+ return self._findAll(name, attrs, text, limit, self.previousGenerator,
+ **kwargs)
+ fetchPrevious = findAllPrevious # Compatibility with pre-3.x
+
+ def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs):
+ """Returns the closest sibling to this Tag that matches the
+ given criteria and appears before this Tag in the document."""
+ return self._findOne(self.findPreviousSiblings, name, attrs, text,
+ **kwargs)
+
+ def findPreviousSiblings(self, name=None, attrs={}, text=None,
+ limit=None, **kwargs):
+ """Returns the siblings of this Tag that match the given
+ criteria and appear before this Tag in the document."""
+ return self._findAll(name, attrs, text, limit,
+ self.previousSiblingGenerator, **kwargs)
+ fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x
+
+ def findParent(self, name=None, attrs={}, **kwargs):
+ """Returns the closest parent of this Tag that matches the given
+ criteria."""
+ # NOTE: We can't use _findOne because findParents takes a different
+ # set of arguments.
+ r = None
+ l = self.findParents(name, attrs, 1)
+ if l:
+ r = l[0]
+ return r
+
+ def findParents(self, name=None, attrs={}, limit=None, **kwargs):
+ """Returns the parents of this Tag that match the given
+ criteria."""
+
+ return self._findAll(name, attrs, None, limit, self.parentGenerator,
+ **kwargs)
+ fetchParents = findParents # Compatibility with pre-3.x
+
+ #These methods do the real heavy lifting.
+
+ def _findOne(self, method, name, attrs, text, **kwargs):
+ r = None
+ l = method(name, attrs, text, 1, **kwargs)
+ if l:
+ r = l[0]
+ return r
+
+ def _findAll(self, name, attrs, text, limit, generator, **kwargs):
+ "Iterates over a generator looking for things that match."
+
+ if isinstance(name, SoupStrainer):
+ strainer = name
+ # (Possibly) special case some findAll*(...) searches
+ elif text is None and not limit and not attrs and not kwargs:
+ # findAll*(True)
+ if name is True:
+ return [element for element in generator()
+ if isinstance(element, Tag)]
+ # findAll*('tag-name')
+ elif isinstance(name, basestring):
+ return [element for element in generator()
+ if isinstance(element, Tag) and
+ element.name == name]
+ else:
+ strainer = SoupStrainer(name, attrs, text, **kwargs)
+ # Build a SoupStrainer
+ else:
+ strainer = SoupStrainer(name, attrs, text, **kwargs)
+ results = ResultSet(strainer)
+ g = generator()
+ while True:
+ try:
+ i = g.next()
+ except StopIteration:
+ break
+ if i:
+ found = strainer.search(i)
+ if found:
+ results.append(found)
+ if limit and len(results) >= limit:
+ break
+ return results
+
+ #These Generators can be used to navigate starting from both
+ #NavigableStrings and Tags.
+ def nextGenerator(self):
+ i = self
+ while i is not None:
+ i = i.next
+ yield i
+
+ def nextSiblingGenerator(self):
+ i = self
+ while i is not None:
+ i = i.nextSibling
+ yield i
+
+ def previousGenerator(self):
+ i = self
+ while i is not None:
+ i = i.previous
+ yield i
+
+ def previousSiblingGenerator(self):
+ i = self
+ while i is not None:
+ i = i.previousSibling
+ yield i
+
+ def parentGenerator(self):
+ i = self
+ while i is not None:
+ i = i.parent
+ yield i
+
+ # Utility methods
+ def substituteEncoding(self, str, encoding=None):
+ encoding = encoding or "utf-8"
+ return str.replace("%SOUP-ENCODING%", encoding)
+
+ def toEncoding(self, s, encoding=None):
+ """Encodes an object to a string in some encoding, or to Unicode.
+ ."""
+ if isinstance(s, unicode):
+ if encoding:
+ s = s.encode(encoding)
+ elif isinstance(s, str):
+ if encoding:
+ s = s.encode(encoding)
+ else:
+ s = unicode(s)
+ else:
+ if encoding:
+ s = self.toEncoding(str(s), encoding)
+ else:
+ s = unicode(s)
+ return s
+
+ BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
+ + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
+ + ")")
+
+ def _sub_entity(self, x):
+ """Used with a regular expression to substitute the
+ appropriate XML entity for an XML special character."""
+ return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";"
+
+
+class NavigableString(unicode, PageElement):
+
+ def __new__(cls, value):
+ """Create a new NavigableString.
+
+ When unpickling a NavigableString, this method is called with
+ the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
+ passed in to the superclass's __new__ or the superclass won't know
+ how to handle non-ASCII characters.
+ """
+ if isinstance(value, unicode):
+ return unicode.__new__(cls, value)
+ return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
+
+ def __getnewargs__(self):
+ return (NavigableString.__str__(self),)
+
+ def __getattr__(self, attr):
+ """text.string gives you text. This is for backwards
+ compatibility for Navigable*String, but for CData* it lets you
+ get the string without the CData wrapper."""
+ if attr == 'string':
+ return self
+ else:
+ raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)
+
+ def __unicode__(self):
+ return str(self).decode(DEFAULT_OUTPUT_ENCODING)
+
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ # Substitute outgoing XML entities.
+ data = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, self)
+ if encoding:
+ return data.encode(encoding)
+ else:
+ return data
+
+class CData(NavigableString):
+
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding)
+
+class ProcessingInstruction(NavigableString):
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ output = self
+ if "%SOUP-ENCODING%" in output:
+ output = self.substituteEncoding(output, encoding)
+ return "<?%s?>" % self.toEncoding(output, encoding)
+
+class Comment(NavigableString):
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ return "<!--%s-->" % NavigableString.__str__(self, encoding)
+
+class Declaration(NavigableString):
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ return "<!%s>" % NavigableString.__str__(self, encoding)
+
+class Tag(PageElement):
+
+ """Represents a found HTML tag with its attributes and contents."""
+
+ def _convertEntities(self, match):
+ """Used in a call to re.sub to replace HTML, XML, and numeric
+ entities with the appropriate Unicode characters. If HTML
+ entities are being converted, any unrecognized entities are
+ escaped."""
+ x = match.group(1)
+ if self.convertHTMLEntities and x in name2codepoint:
+ return unichr(name2codepoint[x])
+ elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS:
+ if self.convertXMLEntities:
+ return self.XML_ENTITIES_TO_SPECIAL_CHARS[x]
+ else:
+ return u'&%s;' % x
+ elif len(x) > 0 and x[0] == '#':
+ # Handle numeric entities
+ if len(x) > 1 and x[1] == 'x':
+ return unichr(int(x[2:], 16))
+ else:
+ return unichr(int(x[1:]))
+
+ elif self.escapeUnrecognizedEntities:
+ return u'&amp;%s;' % x
+ else:
+ return u'&%s;' % x
+
+ def __init__(self, parser, name, attrs=None, parent=None,
+ previous=None):
+ "Basic constructor."
+
+ # We don't actually store the parser object: that lets extracted
+ # chunks be garbage-collected
+ self.parserClass = parser.__class__
+ self.isSelfClosing = parser.isSelfClosingTag(name)
+ self.name = name
+ if attrs is None:
+ attrs = []
+ elif isinstance(attrs, dict):
+ attrs = attrs.items()
+ self.attrs = attrs
+ self.contents = []
+ self.setup(parent, previous)
+ self.hidden = False
+ self.containsSubstitutions = False
+ self.convertHTMLEntities = parser.convertHTMLEntities
+ self.convertXMLEntities = parser.convertXMLEntities
+ self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities
+
+ # Convert any HTML, XML, or numeric entities in the attribute values.
+ convert = lambda(k, val): (k,
+ re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);",
+ self._convertEntities,
+ val))
+ self.attrs = map(convert, self.attrs)
+
+ def getString(self):
+ if (len(self.contents) == 1
+ and isinstance(self.contents[0], NavigableString)):
+ return self.contents[0]
+
+ def setString(self, string):
+ """Replace the contents of the tag with a string"""
+ self.clear()
+ self.append(string)
+
+ string = property(getString, setString)
+
+ def getText(self, separator=u""):
+ if not len(self.contents):
+ return u""
+ stopNode = self._lastRecursiveChild().next
+ strings = []
+ current = self.contents[0]
+ while current is not stopNode:
+ if isinstance(current, NavigableString):
+ strings.append(current.strip())
+ current = current.next
+ return separator.join(strings)
+
+ text = property(getText)
+
+ def get(self, key, default=None):
+ """Returns the value of the 'key' attribute for the tag, or
+ the value given for 'default' if it doesn't have that
+ attribute."""
+ return self._getAttrMap().get(key, default)
+
+ def clear(self):
+ """Extract all children."""
+ for child in self.contents[:]:
+ child.extract()
+
+ def index(self, element):
+ for i, child in enumerate(self.contents):
+ if child is element:
+ return i
+ raise ValueError("Tag.index: element not in tag")
+
+ def has_key(self, key):
+ return self._getAttrMap().has_key(key)
+
+ def __getitem__(self, key):
+ """tag[key] returns the value of the 'key' attribute for the tag,
+ and throws an exception if it's not there."""
+ return self._getAttrMap()[key]
+
+ def __iter__(self):
+ "Iterating over a tag iterates over its contents."
+ return iter(self.contents)
+
+ def __len__(self):
+ "The length of a tag is the length of its list of contents."
+ return len(self.contents)
+
+ def __contains__(self, x):
+ return x in self.contents
+
+ def __nonzero__(self):
+ "A tag is non-None even if it has no contents."
+ return True
+
+ def __setitem__(self, key, value):
+ """Setting tag[key] sets the value of the 'key' attribute for the
+ tag."""
+ self._getAttrMap()
+ self.attrMap[key] = value
+ found = False
+ for i in range(0, len(self.attrs)):
+ if self.attrs[i][0] == key:
+ self.attrs[i] = (key, value)
+ found = True
+ if not found:
+ self.attrs.append((key, value))
+ self._getAttrMap()[key] = value
+
+ def __delitem__(self, key):
+ "Deleting tag[key] deletes all 'key' attributes for the tag."
+ for item in self.attrs:
+ if item[0] == key:
+ self.attrs.remove(item)
+ #We don't break because bad HTML can define the same
+ #attribute multiple times.
+ self._getAttrMap()
+ if self.attrMap.has_key(key):
+ del self.attrMap[key]
+
+ def __call__(self, *args, **kwargs):
+ """Calling a tag like a function is the same as calling its
+ findAll() method. Eg. tag('a') returns a list of all the A tags
+ found within this tag."""
+ return apply(self.findAll, args, kwargs)
+
+ def __getattr__(self, tag):
+ #print "Getattr %s.%s" % (self.__class__, tag)
+ if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3:
+ return self.find(tag[:-3])
+ elif tag.find('__') != 0:
+ return self.find(tag)
+ raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag)
+
+ def __eq__(self, other):
+ """Returns true iff this tag has the same name, the same attributes,
+ and the same contents (recursively) as the given tag.
+
+ NOTE: right now this will return false if two tags have the
+ same attributes in a different order. Should this be fixed?"""
+ if other is self:
+ return True
+ if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other):
+ return False
+ for i in range(0, len(self.contents)):
+ if self.contents[i] != other.contents[i]:
+ return False
+ return True
+
+ def __ne__(self, other):
+ """Returns true iff this tag is not identical to the other tag,
+ as defined in __eq__."""
+ return not self == other
+
+ def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ """Renders this tag as a string."""
+ return self.__str__(encoding)
+
+ def __unicode__(self):
+ return self.__str__(None)
+
+ def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING,
+ prettyPrint=False, indentLevel=0):
+ """Returns a string or Unicode representation of this tag and
+ its contents. To get Unicode, pass None for encoding.
+
+ NOTE: since Python's HTML parser consumes whitespace, this
+ method is not certain to reproduce the whitespace present in
+ the original string."""
+
+ encodedName = self.toEncoding(self.name, encoding)
+
+ attrs = []
+ if self.attrs:
+ for key, val in self.attrs:
+ fmt = '%s="%s"'
+ if isinstance(val, basestring):
+ if self.containsSubstitutions and '%SOUP-ENCODING%' in val:
+ val = self.substituteEncoding(val, encoding)
+
+ # The attribute value either:
+ #
+ # * Contains no embedded double quotes or single quotes.
+ # No problem: we enclose it in double quotes.
+ # * Contains embedded single quotes. No problem:
+ # double quotes work here too.
+ # * Contains embedded double quotes. No problem:
+ # we enclose it in single quotes.
+ # * Embeds both single _and_ double quotes. This
+ # can't happen naturally, but it can happen if
+ # you modify an attribute value after parsing
+ # the document. Now we have a bit of a
+ # problem. We solve it by enclosing the
+ # attribute in single quotes, and escaping any
+ # embedded single quotes to XML entities.
+ if '"' in val:
+ fmt = "%s='%s'"
+ if "'" in val:
+ # TODO: replace with apos when
+ # appropriate.
+ val = val.replace("'", "&squot;")
+
+ # Now we're okay w/r/t quotes. But the attribute
+ # value might also contain angle brackets, or
+ # ampersands that aren't part of entities. We need
+ # to escape those to XML entities too.
+ val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val)
+
+ attrs.append(fmt % (self.toEncoding(key, encoding),
+ self.toEncoding(val, encoding)))
+ close = ''
+ closeTag = ''
+ if self.isSelfClosing:
+ close = ' /'
+ else:
+ closeTag = '</%s>' % encodedName
+
+ indentTag, indentContents = 0, 0
+ if prettyPrint:
+ indentTag = indentLevel
+ space = (' ' * (indentTag-1))
+ indentContents = indentTag + 1
+ contents = self.renderContents(encoding, prettyPrint, indentContents)
+ if self.hidden:
+ s = contents
+ else:
+ s = []
+ attributeString = ''
+ if attrs:
+ attributeString = ' ' + ' '.join(attrs)
+ if prettyPrint:
+ s.append(space)
+ s.append('<%s%s%s>' % (encodedName, attributeString, close))
+ if prettyPrint:
+ s.append("\n")
+ s.append(contents)
+ if prettyPrint and contents and contents[-1] != "\n":
+ s.append("\n")
+ if prettyPrint and closeTag:
+ s.append(space)
+ s.append(closeTag)
+ if prettyPrint and closeTag and self.nextSibling:
+ s.append("\n")
+ s = ''.join(s)
+ return s
+
+ def decompose(self):
+ """Recursively destroys the contents of this tree."""
+ self.extract()
+ if len(self.contents) == 0:
+ return
+ current = self.contents[0]
+ while current is not None:
+ next = current.next
+ if isinstance(current, Tag):
+ del current.contents[:]
+ current.parent = None
+ current.previous = None
+ current.previousSibling = None
+ current.next = None
+ current.nextSibling = None
+ current = next
+
+ def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING):
+ return self.__str__(encoding, True)
+
+ def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
+ prettyPrint=False, indentLevel=0):
+ """Renders the contents of this tag as a string in the given
+ encoding. If encoding is None, returns a Unicode string.."""
+ s=[]
+ for c in self:
+ text = None
+ if isinstance(c, NavigableString):
+ text = c.__str__(encoding)
+ elif isinstance(c, Tag):
+ s.append(c.__str__(encoding, prettyPrint, indentLevel))
+ if text and prettyPrint:
+ text = text.strip()
+ if text:
+ if prettyPrint:
+ s.append(" " * (indentLevel-1))
+ s.append(text)
+ if prettyPrint:
+ s.append("\n")
+ return ''.join(s)
+
+ #Soup methods
+
+ def find(self, name=None, attrs={}, recursive=True, text=None,
+ **kwargs):
+ """Return only the first child of this Tag matching the given
+ criteria."""
+ r = None
+ l = self.findAll(name, attrs, recursive, text, 1, **kwargs)
+ if l:
+ r = l[0]
+ return r
+ findChild = find
+
+ def findAll(self, name=None, attrs={}, recursive=True, text=None,
+ limit=None, **kwargs):
+ """Extracts a list of Tag objects that match the given
+ criteria. You can specify the name of the Tag and any
+ attributes you want the Tag to have.
+
+ The value of a key-value pair in the 'attrs' map can be a
+ string, a list of strings, a regular expression object, or a
+ callable that takes a string and returns whether or not the
+ string matches for some custom definition of 'matches'. The
+ same is true of the tag name."""
+ generator = self.recursiveChildGenerator
+ if not recursive:
+ generator = self.childGenerator
+ return self._findAll(name, attrs, text, limit, generator, **kwargs)
+ findChildren = findAll
+
+ # Pre-3.x compatibility methods
+ first = find
+ fetch = findAll
+
+ def fetchText(self, text=None, recursive=True, limit=None):
+ return self.findAll(text=text, recursive=recursive, limit=limit)
+
+ def firstText(self, text=None, recursive=True):
+ return self.find(text=text, recursive=recursive)
+
+ #Private methods
+
+ def _getAttrMap(self):
+ """Initializes a map representation of this tag's attributes,
+ if not already initialized."""
+ if not getattr(self, 'attrMap'):
+ self.attrMap = {}
+ for (key, value) in self.attrs:
+ self.attrMap[key] = value
+ return self.attrMap
+
+ #Generator methods
+ def childGenerator(self):
+ # Just use the iterator from the contents
+ return iter(self.contents)
+
+ def recursiveChildGenerator(self):
+ if not len(self.contents):
+ raise StopIteration
+ stopNode = self._lastRecursiveChild().next
+ current = self.contents[0]
+ while current is not stopNode:
+ yield current
+ current = current.next
+
+
+# Next, a couple classes to represent queries and their results.
+class SoupStrainer:
+ """Encapsulates a number of ways of matching a markup element (tag or
+ text)."""
+
+ def __init__(self, name=None, attrs={}, text=None, **kwargs):
+ self.name = name
+ if isinstance(attrs, basestring):
+ kwargs['class'] = _match_css_class(attrs)
+ attrs = None
+ if kwargs:
+ if attrs:
+ attrs = attrs.copy()
+ attrs.update(kwargs)
+ else:
+ attrs = kwargs
+ self.attrs = attrs
+ self.text = text
+
+ def __str__(self):
+ if self.text:
+ return self.text
+ else:
+ return "%s|%s" % (self.name, self.attrs)
+
+ def searchTag(self, markupName=None, markupAttrs={}):
+ found = None
+ markup = None
+ if isinstance(markupName, Tag):
+ markup = markupName
+ markupAttrs = markup
+ callFunctionWithTagData = callable(self.name) \
+ and not isinstance(markupName, Tag)
+
+ if (not self.name) \
+ or callFunctionWithTagData \
+ or (markup and self._matches(markup, self.name)) \
+ or (not markup and self._matches(markupName, self.name)):
+ if callFunctionWithTagData:
+ match = self.name(markupName, markupAttrs)
+ else:
+ match = True
+ markupAttrMap = None
+ for attr, matchAgainst in self.attrs.items():
+ if not markupAttrMap:
+ if hasattr(markupAttrs, 'get'):
+ markupAttrMap = markupAttrs
+ else:
+ markupAttrMap = {}
+ for k,v in markupAttrs:
+ markupAttrMap[k] = v
+ attrValue = markupAttrMap.get(attr)
+ if not self._matches(attrValue, matchAgainst):
+ match = False
+ break
+ if match:
+ if markup:
+ found = markup
+ else:
+ found = markupName
+ return found
+
+ def search(self, markup):
+ #print 'looking for %s in %s' % (self, markup)
+ found = None
+ # If given a list of items, scan it for a text element that
+ # matches.
+ if hasattr(markup, "__iter__") \
+ and not isinstance(markup, Tag):
+ for element in markup:
+ if isinstance(element, NavigableString) \
+ and self.search(element):
+ found = element
+ break
+ # If it's a Tag, make sure its name or attributes match.
+ # Don't bother with Tags if we're searching for text.
+ elif isinstance(markup, Tag):
+ if not self.text:
+ found = self.searchTag(markup)
+ # If it's text, make sure the text matches.
+ elif isinstance(markup, NavigableString) or \
+ isinstance(markup, basestring):
+ if self._matches(markup, self.text):
+ found = markup
+ else:
+ raise Exception, "I don't know how to match against a %s" \
+ % markup.__class__
+ return found
+
+ def _matches(self, markup, matchAgainst):
+ #print "Matching %s against %s" % (markup, matchAgainst)
+ result = False
+ if matchAgainst is True:
+ result = markup is not None
+ elif callable(matchAgainst):
+ result = matchAgainst(markup)
+ else:
+ #Custom match methods take the tag as an argument, but all
+ #other ways of matching match the tag name as a string.
+ if isinstance(markup, Tag):
+ markup = markup.name
+ if markup and not isinstance(markup, basestring):
+ markup = unicode(markup)
+ #Now we know that chunk is either a string, or None.
+ if hasattr(matchAgainst, 'match'):
+ # It's a regexp object.
+ result = markup and matchAgainst.search(markup)
+ elif hasattr(matchAgainst, '__iter__'): # list-like
+ result = markup in matchAgainst
+ elif hasattr(matchAgainst, 'items'):
+ result = markup.has_key(matchAgainst)
+ elif matchAgainst and isinstance(markup, basestring):
+ if isinstance(markup, unicode):
+ matchAgainst = unicode(matchAgainst)
+ else:
+ matchAgainst = str(matchAgainst)
+
+ if not result:
+ result = matchAgainst == markup
+ return result
+
+class ResultSet(list):
+ """A ResultSet is just a list that keeps track of the SoupStrainer
+ that created it."""
+ def __init__(self, source):
+ list.__init__([])
+ self.source = source
+
+# Now, some helper functions.
+
+def buildTagMap(default, *args):
+ """Turns a list of maps, lists, or scalars into a single map.
+ Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and
+ NESTING_RESET_TAGS maps out of lists and partial maps."""
+ built = {}
+ for portion in args:
+ if hasattr(portion, 'items'):
+ #It's a map. Merge it.
+ for k,v in portion.items():
+ built[k] = v
+ elif hasattr(portion, '__iter__'): # is a list
+ #It's a list. Map each item to the default.
+ for k in portion:
+ built[k] = default
+ else:
+ #It's a scalar. Map it to the default.
+ built[portion] = default
+ return built
+
+# Now, the parser classes.
+
+class BeautifulStoneSoup(Tag, SGMLParser):
+
+ """This class contains the basic parser and search code. It defines
+ a parser that knows nothing about tag behavior except for the
+ following:
+
+ You can't close a tag without closing all the tags it encloses.
+ That is, "<foo><bar></foo>" actually means
+ "<foo><bar></bar></foo>".
+
+ [Another possible explanation is "<foo><bar /></foo>", but since
+ this class defines no SELF_CLOSING_TAGS, it will never use that
+ explanation.]
+
+ This class is useful for parsing XML or made-up markup languages,
+ or when BeautifulSoup makes an assumption counter to what you were
+ expecting."""
+
+ SELF_CLOSING_TAGS = {}
+ NESTABLE_TAGS = {}
+ RESET_NESTING_TAGS = {}
+ QUOTE_TAGS = {}
+ PRESERVE_WHITESPACE_TAGS = []
+
+ MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'),
+ lambda x: x.group(1) + ' />'),
+ (re.compile('<!\s+([^<>]*)>'),
+ lambda x: '<!' + x.group(1) + '>')
+ ]
+
+ ROOT_TAG_NAME = u'[document]'
+
+ HTML_ENTITIES = "html"
+ XML_ENTITIES = "xml"
+ XHTML_ENTITIES = "xhtml"
+ # TODO: This only exists for backwards-compatibility
+ ALL_ENTITIES = XHTML_ENTITIES
+
+ # Used when determining whether a text node is all whitespace and
+ # can be replaced with a single space. A text node that contains
+ # fancy Unicode spaces (usually non-breaking) should be left
+ # alone.
+ STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, }
+
+ def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None,
+ markupMassage=True, smartQuotesTo=XML_ENTITIES,
+ convertEntities=None, selfClosingTags=None, isHTML=False):
+ """The Soup object is initialized as the 'root tag', and the
+ provided markup (which can be a string or a file-like object)
+ is fed into the underlying parser.
+
+ sgmllib will process most bad HTML, and the BeautifulSoup
+ class has some tricks for dealing with some HTML that kills
+ sgmllib, but Beautiful Soup can nonetheless choke or lose data
+ if your data uses self-closing tags or declarations
+ incorrectly.
+
+ By default, Beautiful Soup uses regexes to sanitize input,
+ avoiding the vast majority of these problems. If the problems
+ don't apply to you, pass in False for markupMassage, and
+ you'll get better performance.
+
+ The default parser massage techniques fix the two most common
+ instances of invalid HTML that choke sgmllib:
+
+ <br/> (No space between name of closing tag and tag close)
+ <! --Comment--> (Extraneous whitespace in declaration)
+
+ You can pass in a custom list of (RE object, replace method)
+ tuples to get Beautiful Soup to scrub your input the way you
+ want."""
+
+ self.parseOnlyThese = parseOnlyThese
+ self.fromEncoding = fromEncoding
+ self.smartQuotesTo = smartQuotesTo
+ self.convertEntities = convertEntities
+ # Set the rules for how we'll deal with the entities we
+ # encounter
+ if self.convertEntities:
+ # It doesn't make sense to convert encoded characters to
+ # entities even while you're converting entities to Unicode.
+ # Just convert it all to Unicode.
+ self.smartQuotesTo = None
+ if convertEntities == self.HTML_ENTITIES:
+ self.convertXMLEntities = False
+ self.convertHTMLEntities = True
+ self.escapeUnrecognizedEntities = True
+ elif convertEntities == self.XHTML_ENTITIES:
+ self.convertXMLEntities = True
+ self.convertHTMLEntities = True
+ self.escapeUnrecognizedEntities = False
+ elif convertEntities == self.XML_ENTITIES:
+ self.convertXMLEntities = True
+ self.convertHTMLEntities = False
+ self.escapeUnrecognizedEntities = False
+ else:
+ self.convertXMLEntities = False
+ self.convertHTMLEntities = False
+ self.escapeUnrecognizedEntities = False
+
+ self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags)
+ SGMLParser.__init__(self)
+
+ if hasattr(markup, 'read'): # It's a file-type object.
+ markup = markup.read()
+ self.markup = markup
+ self.markupMassage = markupMassage
+ try:
+ self._feed(isHTML=isHTML)
+ except StopParsing:
+ pass
+ self.markup = None # The markup can now be GCed
+
+ def convert_charref(self, name):
+ """This method fixes a bug in Python's SGMLParser."""
+ try:
+ n = int(name)
+ except ValueError:
+ return
+ if not 0 <= n <= 127 : # ASCII ends at 127, not 255
+ return
+ return self.convert_codepoint(n)
+
+ def _feed(self, inDocumentEncoding=None, isHTML=False):
+ # Convert the document to Unicode.
+ markup = self.markup
+ if isinstance(markup, unicode):
+ if not hasattr(self, 'originalEncoding'):
+ self.originalEncoding = None
+ else:
+ dammit = UnicodeDammit\
+ (markup, [self.fromEncoding, inDocumentEncoding],
+ smartQuotesTo=self.smartQuotesTo, isHTML=isHTML)
+ markup = dammit.unicode
+ self.originalEncoding = dammit.originalEncoding
+ self.declaredHTMLEncoding = dammit.declaredHTMLEncoding
+ if markup:
+ if self.markupMassage:
+ if not hasattr(self.markupMassage, "__iter__"):
+ self.markupMassage = self.MARKUP_MASSAGE
+ for fix, m in self.markupMassage:
+ markup = fix.sub(m, markup)
+ # TODO: We get rid of markupMassage so that the
+ # soup object can be deepcopied later on. Some
+ # Python installations can't copy regexes. If anyone
+ # was relying on the existence of markupMassage, this
+ # might cause problems.
+ del(self.markupMassage)
+ self.reset()
+
+ SGMLParser.feed(self, markup)
+ # Close out any unfinished strings and close all the open tags.
+ self.endData()
+ while self.currentTag.name != self.ROOT_TAG_NAME:
+ self.popTag()
+
+ def __getattr__(self, methodName):
+ """This method routes method call requests to either the SGMLParser
+ superclass or the Tag superclass, depending on the method name."""
+ #print "__getattr__ called on %s.%s" % (self.__class__, methodName)
+
+ if methodName.startswith('start_') or methodName.startswith('end_') \
+ or methodName.startswith('do_'):
+ return SGMLParser.__getattr__(self, methodName)
+ elif not methodName.startswith('__'):
+ return Tag.__getattr__(self, methodName)
+ else:
+ raise AttributeError
+
+ def isSelfClosingTag(self, name):
+ """Returns true iff the given string is the name of a
+ self-closing tag according to this parser."""
+ return self.SELF_CLOSING_TAGS.has_key(name) \
+ or self.instanceSelfClosingTags.has_key(name)
+
+ def reset(self):
+ Tag.__init__(self, self, self.ROOT_TAG_NAME)
+ self.hidden = 1
+ SGMLParser.reset(self)
+ self.currentData = []
+ self.currentTag = None
+ self.tagStack = []
+ self.quoteStack = []
+ self.pushTag(self)
+
+ def popTag(self):
+ tag = self.tagStack.pop()
+
+ #print "Pop", tag.name
+ if self.tagStack:
+ self.currentTag = self.tagStack[-1]
+ return self.currentTag
+
+ def pushTag(self, tag):
+ #print "Push", tag.name
+ if self.currentTag:
+ self.currentTag.contents.append(tag)
+ self.tagStack.append(tag)
+ self.currentTag = self.tagStack[-1]
+
+ def endData(self, containerClass=NavigableString):
+ if self.currentData:
+ currentData = u''.join(self.currentData)
+ if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
+ not set([tag.name for tag in self.tagStack]).intersection(
+ self.PRESERVE_WHITESPACE_TAGS)):
+ if '\n' in currentData:
+ currentData = '\n'
+ else:
+ currentData = ' '
+ self.currentData = []
+ if self.parseOnlyThese and len(self.tagStack) <= 1 and \
+ (not self.parseOnlyThese.text or \
+ not self.parseOnlyThese.search(currentData)):
+ return
+ o = containerClass(currentData)
+ o.setup(self.currentTag, self.previous)
+ if self.previous:
+ self.previous.next = o
+ self.previous = o
+ self.currentTag.contents.append(o)
+
+
+ def _popToTag(self, name, inclusivePop=True):
+ """Pops the tag stack up to and including the most recent
+ instance of the given tag. If inclusivePop is false, pops the tag
+ stack up to but *not* including the most recent instqance of
+ the given tag."""
+ #print "Popping to %s" % name
+ if name == self.ROOT_TAG_NAME:
+ return
+
+ numPops = 0
+ mostRecentTag = None
+ for i in range(len(self.tagStack)-1, 0, -1):
+ if name == self.tagStack[i].name:
+ numPops = len(self.tagStack)-i
+ break
+ if not inclusivePop:
+ numPops = numPops - 1
+
+ for i in range(0, numPops):
+ mostRecentTag = self.popTag()
+ return mostRecentTag
+
+ def _smartPop(self, name):
+
+ """We need to pop up to the previous tag of this type, unless
+ one of this tag's nesting reset triggers comes between this
+ tag and the previous tag of this type, OR unless this tag is a
+ generic nesting trigger and another generic nesting trigger
+ comes between this tag and the previous tag of this type.
+
+ Examples:
+ <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'.
+ <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'.
+ <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'.
+
+ <li><ul><li> *<li>* should pop to 'ul', not the first 'li'.
+ <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr'
+ <td><tr><td> *<td>* should pop to 'tr', not the first 'td'
+ """
+
+ nestingResetTriggers = self.NESTABLE_TAGS.get(name)
+ isNestable = nestingResetTriggers != None
+ isResetNesting = self.RESET_NESTING_TAGS.has_key(name)
+ popTo = None
+ inclusive = True
+ for i in range(len(self.tagStack)-1, 0, -1):
+ p = self.tagStack[i]
+ if (not p or p.name == name) and not isNestable:
+ #Non-nestable tags get popped to the top or to their
+ #last occurance.
+ popTo = name
+ break
+ if (nestingResetTriggers is not None
+ and p.name in nestingResetTriggers) \
+ or (nestingResetTriggers is None and isResetNesting
+ and self.RESET_NESTING_TAGS.has_key(p.name)):
+
+ #If we encounter one of the nesting reset triggers
+ #peculiar to this tag, or we encounter another tag
+ #that causes nesting to reset, pop up to but not
+ #including that tag.
+ popTo = p.name
+ inclusive = False
+ break
+ p = p.parent
+ if popTo:
+ self._popToTag(popTo, inclusive)
+
+ def unknown_starttag(self, name, attrs, selfClosing=0):
+ #print "Start tag %s: %s" % (name, attrs)
+ if self.quoteStack:
+ #This is not a real tag.
+ #print "<%s> is not real!" % name
+ attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs])
+ self.handle_data('<%s%s>' % (name, attrs))
+ return
+ self.endData()
+
+ if not self.isSelfClosingTag(name) and not selfClosing:
+ self._smartPop(name)
+
+ if self.parseOnlyThese and len(self.tagStack) <= 1 \
+ and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)):
+ return
+
+ tag = Tag(self, name, attrs, self.currentTag, self.previous)
+ if self.previous:
+ self.previous.next = tag
+ self.previous = tag
+ self.pushTag(tag)
+ if selfClosing or self.isSelfClosingTag(name):
+ self.popTag()
+ if name in self.QUOTE_TAGS:
+ #print "Beginning quote (%s)" % name
+ self.quoteStack.append(name)
+ self.literal = 1
+ return tag
+
+ def unknown_endtag(self, name):
+ #print "End tag %s" % name
+ if self.quoteStack and self.quoteStack[-1] != name:
+ #This is not a real end tag.
+ #print "</%s> is not real!" % name
+ self.handle_data('</%s>' % name)
+ return
+ self.endData()
+ self._popToTag(name)
+ if self.quoteStack and self.quoteStack[-1] == name:
+ self.quoteStack.pop()
+ self.literal = (len(self.quoteStack) > 0)
+
+ def handle_data(self, data):
+ self.currentData.append(data)
+
+ def _toStringSubclass(self, text, subclass):
+ """Adds a certain piece of text to the tree as a NavigableString
+ subclass."""
+ self.endData()
+ self.handle_data(text)
+ self.endData(subclass)
+
+ def handle_pi(self, text):
+ """Handle a processing instruction as a ProcessingInstruction
+ object, possibly one with a %SOUP-ENCODING% slot into which an
+ encoding will be plugged later."""
+ if text[:3] == "xml":
+ text = u"xml version='1.0' encoding='%SOUP-ENCODING%'"
+ self._toStringSubclass(text, ProcessingInstruction)
+
+ def handle_comment(self, text):
+ "Handle comments as Comment objects."
+ self._toStringSubclass(text, Comment)
+
+ def handle_charref(self, ref):
+ "Handle character references as data."
+ if self.convertEntities:
+ data = unichr(int(ref))
+ else:
+ data = '&#%s;' % ref
+ self.handle_data(data)
+
+ def handle_entityref(self, ref):
+ """Handle entity references as data, possibly converting known
+ HTML and/or XML entity references to the corresponding Unicode
+ characters."""
+ data = None
+ if self.convertHTMLEntities:
+ try:
+ data = unichr(name2codepoint[ref])
+ except KeyError:
+ pass
+
+ if not data and self.convertXMLEntities:
+ data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref)
+
+ if not data and self.convertHTMLEntities and \
+ not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref):
+ # TODO: We've got a problem here. We're told this is
+ # an entity reference, but it's not an XML entity
+ # reference or an HTML entity reference. Nonetheless,
+ # the logical thing to do is to pass it through as an
+ # unrecognized entity reference.
+ #
+ # Except: when the input is "&carol;" this function
+ # will be called with input "carol". When the input is
+ # "AT&T", this function will be called with input
+ # "T". We have no way of knowing whether a semicolon
+ # was present originally, so we don't know whether
+ # this is an unknown entity or just a misplaced
+ # ampersand.
+ #
+ # The more common case is a misplaced ampersand, so I
+ # escape the ampersand and omit the trailing semicolon.
+ data = "&amp;%s" % ref
+ if not data:
+ # This case is different from the one above, because we
+ # haven't already gone through a supposedly comprehensive
+ # mapping of entities to Unicode characters. We might not
+ # have gone through any mapping at all. So the chances are
+ # very high that this is a real entity, and not a
+ # misplaced ampersand.
+ data = "&%s;" % ref
+ self.handle_data(data)
+
+ def handle_decl(self, data):
+ "Handle DOCTYPEs and the like as Declaration objects."
+ self._toStringSubclass(data, Declaration)
+
+ def parse_declaration(self, i):
+ """Treat a bogus SGML declaration as raw data. Treat a CDATA
+ declaration as a CData object."""
+ j = None
+ if self.rawdata[i:i+9] == '<![CDATA[':
+ k = self.rawdata.find(']]>', i)
+ if k == -1:
+ k = len(self.rawdata)
+ data = self.rawdata[i+9:k]
+ j = k+3
+ self._toStringSubclass(data, CData)
+ else:
+ try:
+ j = SGMLParser.parse_declaration(self, i)
+ except SGMLParseError:
+ toHandle = self.rawdata[i:]
+ self.handle_data(toHandle)
+ j = i + len(toHandle)
+ return j
+
+class BeautifulSoup(BeautifulStoneSoup):
+
+ """This parser knows the following facts about HTML:
+
+ * Some tags have no closing tag and should be interpreted as being
+ closed as soon as they are encountered.
+
+ * The text inside some tags (ie. 'script') may contain tags which
+ are not really part of the document and which should be parsed
+ as text, not tags. If you want to parse the text as tags, you can
+ always fetch it and parse it explicitly.
+
+ * Tag nesting rules:
+
+ Most tags can't be nested at all. For instance, the occurance of
+ a <p> tag should implicitly close the previous <p> tag.
+
+ <p>Para1<p>Para2
+ should be transformed into:
+ <p>Para1</p><p>Para2
+
+ Some tags can be nested arbitrarily. For instance, the occurance
+ of a <blockquote> tag should _not_ implicitly close the previous
+ <blockquote> tag.
+
+ Alice said: <blockquote>Bob said: <blockquote>Blah
+ should NOT be transformed into:
+ Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah
+
+ Some tags can be nested, but the nesting is reset by the
+ interposition of other tags. For instance, a <tr> tag should
+ implicitly close the previous <tr> tag within the same <table>,
+ but not close a <tr> tag in another table.
+
+ <table><tr>Blah<tr>Blah
+ should be transformed into:
+ <table><tr>Blah</tr><tr>Blah
+ but,
+ <tr>Blah<table><tr>Blah
+ should NOT be transformed into
+ <tr>Blah<table></tr><tr>Blah
+
+ Differing assumptions about tag nesting rules are a major source
+ of problems with the BeautifulSoup class. If BeautifulSoup is not
+ treating as nestable a tag your page author treats as nestable,
+ try ICantBelieveItsBeautifulSoup, MinimalSoup, or
+ BeautifulStoneSoup before writing your own subclass."""
+
+ def __init__(self, *args, **kwargs):
+ if not kwargs.has_key('smartQuotesTo'):
+ kwargs['smartQuotesTo'] = self.HTML_ENTITIES
+ kwargs['isHTML'] = True
+ BeautifulStoneSoup.__init__(self, *args, **kwargs)
+
+ SELF_CLOSING_TAGS = buildTagMap(None,
+ ('br' , 'hr', 'input', 'img', 'meta',
+ 'spacer', 'link', 'frame', 'base', 'col'))
+
+ PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
+
+ QUOTE_TAGS = {'script' : None, 'textarea' : None}
+
+ #According to the HTML standard, each of these inline tags can
+ #contain another tag of the same type. Furthermore, it's common
+ #to actually use these tags this way.
+ NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
+ 'center')
+
+ #According to the HTML standard, these block tags can contain
+ #another tag of the same type. Furthermore, it's common
+ #to actually use these tags this way.
+ NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')
+
+ #Lists can contain other lists, but there are restrictions.
+ NESTABLE_LIST_TAGS = { 'ol' : [],
+ 'ul' : [],
+ 'li' : ['ul', 'ol'],
+ 'dl' : [],
+ 'dd' : ['dl'],
+ 'dt' : ['dl'] }
+
+ #Tables can contain other tables, but there are restrictions.
+ NESTABLE_TABLE_TAGS = {'table' : [],
+ 'tr' : ['table', 'tbody', 'tfoot', 'thead'],
+ 'td' : ['tr'],
+ 'th' : ['tr'],
+ 'thead' : ['table'],
+ 'tbody' : ['table'],
+ 'tfoot' : ['table'],
+ }
+
+ NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre')
+
+ #If one of these tags is encountered, all tags up to the next tag of
+ #this type are popped.
+ RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript',
+ NON_NESTABLE_BLOCK_TAGS,
+ NESTABLE_LIST_TAGS,
+ NESTABLE_TABLE_TAGS)
+
+ NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS,
+ NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS)
+
+ # Used to detect the charset in a META tag; see start_meta
+ CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
+
+ def start_meta(self, attrs):
+ """Beautiful Soup can detect a charset included in a META tag,
+ try to convert the document to that charset, and re-parse the
+ document from the beginning."""
+ httpEquiv = None
+ contentType = None
+ contentTypeIndex = None
+ tagNeedsEncodingSubstitution = False
+
+ for i in range(0, len(attrs)):
+ key, value = attrs[i]
+ key = key.lower()
+ if key == 'http-equiv':
+ httpEquiv = value
+ elif key == 'content':
+ contentType = value
+ contentTypeIndex = i
+
+ if httpEquiv and contentType: # It's an interesting meta tag.
+ match = self.CHARSET_RE.search(contentType)
+ if match:
+ if (self.declaredHTMLEncoding is not None or
+ self.originalEncoding == self.fromEncoding):
+ # An HTML encoding was sniffed while converting
+ # the document to Unicode, or an HTML encoding was
+ # sniffed during a previous pass through the
+ # document, or an encoding was specified
+ # explicitly and it worked. Rewrite the meta tag.
+ def rewrite(match):
+ return match.group(1) + "%SOUP-ENCODING%"
+ newAttr = self.CHARSET_RE.sub(rewrite, contentType)
+ attrs[contentTypeIndex] = (attrs[contentTypeIndex][0],
+ newAttr)
+ tagNeedsEncodingSubstitution = True
+ else:
+ # This is our first pass through the document.
+ # Go through it again with the encoding information.
+ newCharset = match.group(3)
+ if newCharset and newCharset != self.originalEncoding:
+ self.declaredHTMLEncoding = newCharset
+ self._feed(self.declaredHTMLEncoding)
+ raise StopParsing
+ pass
+ tag = self.unknown_starttag("meta", attrs)
+ if tag and tagNeedsEncodingSubstitution:
+ tag.containsSubstitutions = True
+
+class StopParsing(Exception):
+ pass
+
+class ICantBelieveItsBeautifulSoup(BeautifulSoup):
+
+ """The BeautifulSoup class is oriented towards skipping over
+ common HTML errors like unclosed tags. However, sometimes it makes
+ errors of its own. For instance, consider this fragment:
+
+ <b>Foo<b>Bar</b></b>
+
+ This is perfectly valid (if bizarre) HTML. However, the
+ BeautifulSoup class will implicitly close the first b tag when it
+ encounters the second 'b'. It will think the author wrote
+ "<b>Foo<b>Bar", and didn't close the first 'b' tag, because
+ there's no real-world reason to bold something that's already
+ bold. When it encounters '</b></b>' it will close two more 'b'
+ tags, for a grand total of three tags closed instead of two. This
+ can throw off the rest of your document structure. The same is
+ true of a number of other tags, listed below.
+
+ It's much more common for someone to forget to close a 'b' tag
+ than to actually use nested 'b' tags, and the BeautifulSoup class
+ handles the common case. This class handles the not-co-common
+ case: where you can't believe someone wrote what they did, but
+ it's valid HTML and BeautifulSoup screwed up by assuming it
+ wouldn't be."""
+
+ I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \
+ ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong',
+ 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b',
+ 'big')
+
+ I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',)
+
+ NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS,
+ I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS,
+ I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS)
+
+class MinimalSoup(BeautifulSoup):
+ """The MinimalSoup class is for parsing HTML that contains
+ pathologically bad markup. It makes no assumptions about tag
+ nesting, but it does know which tags are self-closing, that
+ <script> tags contain Javascript and should not be parsed, that
+ META tags may contain encoding information, and so on.
+
+ This also makes it better for subclassing than BeautifulStoneSoup
+ or BeautifulSoup."""
+
+ RESET_NESTING_TAGS = buildTagMap('noscript')
+ NESTABLE_TAGS = {}
+
+class BeautifulSOAP(BeautifulStoneSoup):
+ """This class will push a tag with only a single string child into
+ the tag's parent as an attribute. The attribute's name is the tag
+ name, and the value is the string child. An example should give
+ the flavor of the change:
+
+ <foo><bar>baz</bar></foo>
+ =>
+ <foo bar="baz"><bar>baz</bar></foo>
+
+ You can then access fooTag['bar'] instead of fooTag.barTag.string.
+
+ This is, of course, useful for scraping structures that tend to
+ use subelements instead of attributes, such as SOAP messages. Note
+ that it modifies its input, so don't print the modified version
+ out.
+
+ I'm not sure how many people really want to use this class; let me
+ know if you do. Mainly I like the name."""
+
+ def popTag(self):
+ if len(self.tagStack) > 1:
+ tag = self.tagStack[-1]
+ parent = self.tagStack[-2]
+ parent._getAttrMap()
+ if (isinstance(tag, Tag) and len(tag.contents) == 1 and
+ isinstance(tag.contents[0], NavigableString) and
+ not parent.attrMap.has_key(tag.name)):
+ parent[tag.name] = tag.contents[0]
+ BeautifulStoneSoup.popTag(self)
+
+#Enterprise class names! It has come to our attention that some people
+#think the names of the Beautiful Soup parser classes are too silly
+#and "unprofessional" for use in enterprise screen-scraping. We feel
+#your pain! For such-minded folk, the Beautiful Soup Consortium And
+#All-Night Kosher Bakery recommends renaming this file to
+#"RobustParser.py" (or, in cases of extreme enterprisiness,
+#"RobustParserBeanInterface.class") and using the following
+#enterprise-friendly class aliases:
+class RobustXMLParser(BeautifulStoneSoup):
+ pass
+class RobustHTMLParser(BeautifulSoup):
+ pass
+class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup):
+ pass
+class RobustInsanelyWackAssHTMLParser(MinimalSoup):
+ pass
+class SimplifyingSOAPParser(BeautifulSOAP):
+ pass
+
+######################################################
+#
+# Bonus library: Unicode, Dammit
+#
+# This class forces XML data into a standard format (usually to UTF-8
+# or Unicode). It is heavily based on code from Mark Pilgrim's
+# Universal Feed Parser. It does not rewrite the XML or HTML to
+# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi
+# (XML) and BeautifulSoup.start_meta (HTML).
+
+# Autodetects character encodings.
+# Download from http://chardet.feedparser.org/
+try:
+ import chardet
+# import chardet.constants
+# chardet.constants._debug = 1
+except ImportError:
+ chardet = None
+
+# cjkcodecs and iconv_codec make Python know about more character encodings.
+# Both are available from http://cjkpython.i18n.org/
+# They're built in if you use Python 2.4.
+try:
+ import cjkcodecs.aliases
+except ImportError:
+ pass
+try:
+ import iconv_codec
+except ImportError:
+ pass
+
+class UnicodeDammit:
+ """A class for detecting the encoding of a *ML document and
+ converting it to a Unicode string. If the source encoding is
+ windows-1252, can replace MS smart quotes with their HTML or XML
+ equivalents."""
+
+ # This dictionary maps commonly seen values for "charset" in HTML
+ # meta tags to the corresponding Python codec names. It only covers
+ # values that aren't in Python's aliases and can't be determined
+ # by the heuristics in find_codec.
+ CHARSET_ALIASES = { "macintosh" : "mac-roman",
+ "x-sjis" : "shift-jis" }
+
+ def __init__(self, markup, overrideEncodings=[],
+ smartQuotesTo='xml', isHTML=False):
+ self.declaredHTMLEncoding = None
+ self.markup, documentEncoding, sniffedEncoding = \
+ self._detectEncoding(markup, isHTML)
+ self.smartQuotesTo = smartQuotesTo
+ self.triedEncodings = []
+ if markup == '' or isinstance(markup, unicode):
+ self.originalEncoding = None
+ self.unicode = unicode(markup)
+ return
+
+ u = None
+ for proposedEncoding in overrideEncodings:
+ u = self._convertFrom(proposedEncoding)
+ if u: break
+ if not u:
+ for proposedEncoding in (documentEncoding, sniffedEncoding):
+ u = self._convertFrom(proposedEncoding)
+ if u: break
+
+ # If no luck and we have auto-detection library, try that:
+ if not u and chardet and not isinstance(self.markup, unicode):
+ u = self._convertFrom(chardet.detect(self.markup)['encoding'])
+
+ # As a last resort, try utf-8 and windows-1252:
+ if not u:
+ for proposed_encoding in ("utf-8", "windows-1252"):
+ u = self._convertFrom(proposed_encoding)
+ if u: break
+
+ self.unicode = u
+ if not u: self.originalEncoding = None
+
+ def _subMSChar(self, orig):
+ """Changes a MS smart quote character to an XML or HTML
+ entity."""
+ sub = self.MS_CHARS.get(orig)
+ if isinstance(sub, tuple):
+ if self.smartQuotesTo == 'xml':
+ sub = '&#x%s;' % sub[1]
+ else:
+ sub = '&%s;' % sub[0]
+ return sub
+
+ def _convertFrom(self, proposed):
+ proposed = self.find_codec(proposed)
+ if not proposed or proposed in self.triedEncodings:
+ return None
+ self.triedEncodings.append(proposed)
+ markup = self.markup
+
+ # Convert smart quotes to HTML if coming from an encoding
+ # that might have them.
+ if self.smartQuotesTo and proposed.lower() in("windows-1252",
+ "iso-8859-1",
+ "iso-8859-2"):
+ markup = re.compile("([\x80-\x9f])").sub \
+ (lambda(x): self._subMSChar(x.group(1)),
+ markup)
+
+ try:
+ # print "Trying to convert document to %s" % proposed
+ u = self._toUnicode(markup, proposed)
+ self.markup = u
+ self.originalEncoding = proposed
+ except Exception, e:
+ # print "That didn't work!"
+ # print e
+ return None
+ #print "Correct encoding: %s" % proposed
+ return self.markup
+
+ def _toUnicode(self, data, encoding):
+ '''Given a string and its encoding, decodes the string into Unicode.
+ %encoding is a string recognized by encodings.aliases'''
+
+ # strip Byte Order Mark (if present)
+ if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
+ and (data[2:4] != '\x00\x00'):
+ encoding = 'utf-16be'
+ data = data[2:]
+ elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
+ and (data[2:4] != '\x00\x00'):
+ encoding = 'utf-16le'
+ data = data[2:]
+ elif data[:3] == '\xef\xbb\xbf':
+ encoding = 'utf-8'
+ data = data[3:]
+ elif data[:4] == '\x00\x00\xfe\xff':
+ encoding = 'utf-32be'
+ data = data[4:]
+ elif data[:4] == '\xff\xfe\x00\x00':
+ encoding = 'utf-32le'
+ data = data[4:]
+ newdata = unicode(data, encoding)
+ return newdata
+
+ def _detectEncoding(self, xml_data, isHTML=False):
+ """Given a document, tries to detect its XML encoding."""
+ xml_encoding = sniffed_xml_encoding = None
+ try:
+ if xml_data[:4] == '\x4c\x6f\xa7\x94':
+ # EBCDIC
+ xml_data = self._ebcdic_to_ascii(xml_data)
+ elif xml_data[:4] == '\x00\x3c\x00\x3f':
+ # UTF-16BE
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \
+ and (xml_data[2:4] != '\x00\x00'):
+ # UTF-16BE with BOM
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x3f\x00':
+ # UTF-16LE
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \
+ (xml_data[2:4] != '\x00\x00'):
+ # UTF-16LE with BOM
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\x00\x3c':
+ # UTF-32BE
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x00\x00':
+ # UTF-32LE
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\xfe\xff':
+ # UTF-32BE with BOM
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\xff\xfe\x00\x00':
+ # UTF-32LE with BOM
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
+ elif xml_data[:3] == '\xef\xbb\xbf':
+ # UTF-8 with BOM
+ sniffed_xml_encoding = 'utf-8'
+ xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
+ else:
+ sniffed_xml_encoding = 'ascii'
+ pass
+ except:
+ xml_encoding_match = None
+ xml_encoding_match = re.compile(
+ '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
+ if not xml_encoding_match and isHTML:
+ regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I)
+ xml_encoding_match = regexp.search(xml_data)
+ if xml_encoding_match is not None:
+ xml_encoding = xml_encoding_match.groups()[0].lower()
+ if isHTML:
+ self.declaredHTMLEncoding = xml_encoding
+ if sniffed_xml_encoding and \
+ (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
+ 'iso-10646-ucs-4', 'ucs-4', 'csucs4',
+ 'utf-16', 'utf-32', 'utf_16', 'utf_32',
+ 'utf16', 'u16')):
+ xml_encoding = sniffed_xml_encoding
+ return xml_data, xml_encoding, sniffed_xml_encoding
+
+
+ def find_codec(self, charset):
+ return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
+ or (charset and self._codec(charset.replace("-", ""))) \
+ or (charset and self._codec(charset.replace("-", "_"))) \
+ or charset
+
+ def _codec(self, charset):
+ if not charset: return charset
+ codec = None
+ try:
+ codecs.lookup(charset)
+ codec = charset
+ except (LookupError, ValueError):
+ pass
+ return codec
+
+ EBCDIC_TO_ASCII_MAP = None
+ def _ebcdic_to_ascii(self, s):
+ c = self.__class__
+ if not c.EBCDIC_TO_ASCII_MAP:
+ emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
+ 16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
+ 128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
+ 144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
+ 32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
+ 38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
+ 45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
+ 186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
+ 195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,
+ 201,202,106,107,108,109,110,111,112,113,114,203,204,205,
+ 206,207,208,209,126,115,116,117,118,119,120,121,122,210,
+ 211,212,213,214,215,216,217,218,219,220,221,222,223,224,
+ 225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72,
+ 73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81,
+ 82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89,
+ 90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57,
+ 250,251,252,253,254,255)
+ import string
+ c.EBCDIC_TO_ASCII_MAP = string.maketrans( \
+ ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
+ return s.translate(c.EBCDIC_TO_ASCII_MAP)
+
+ MS_CHARS = { '\x80' : ('euro', '20AC'),
+ '\x81' : ' ',
+ '\x82' : ('sbquo', '201A'),
+ '\x83' : ('fnof', '192'),
+ '\x84' : ('bdquo', '201E'),
+ '\x85' : ('hellip', '2026'),
+ '\x86' : ('dagger', '2020'),
+ '\x87' : ('Dagger', '2021'),
+ '\x88' : ('circ', '2C6'),
+ '\x89' : ('permil', '2030'),
+ '\x8A' : ('Scaron', '160'),
+ '\x8B' : ('lsaquo', '2039'),
+ '\x8C' : ('OElig', '152'),
+ '\x8D' : '?',
+ '\x8E' : ('#x17D', '17D'),
+ '\x8F' : '?',
+ '\x90' : '?',
+ '\x91' : ('lsquo', '2018'),
+ '\x92' : ('rsquo', '2019'),
+ '\x93' : ('ldquo', '201C'),
+ '\x94' : ('rdquo', '201D'),
+ '\x95' : ('bull', '2022'),
+ '\x96' : ('ndash', '2013'),
+ '\x97' : ('mdash', '2014'),
+ '\x98' : ('tilde', '2DC'),
+ '\x99' : ('trade', '2122'),
+ '\x9a' : ('scaron', '161'),
+ '\x9b' : ('rsaquo', '203A'),
+ '\x9c' : ('oelig', '153'),
+ '\x9d' : '?',
+ '\x9e' : ('#x17E', '17E'),
+ '\x9f' : ('Yuml', ''),}
+
+#######################################################################
+
+
+#By default, act as an HTML pretty-printer.
+if __name__ == '__main__':
+ import sys
+ soup = BeautifulSoup(sys.stdin)
+ print soup.prettify()
diff --git a/cgi/GeoIP.dat b/cgi/GeoIP.dat
new file mode 100644
index 0000000..b98993c
--- /dev/null
+++ b/cgi/GeoIP.dat
Binary files differ
diff --git a/cgi/anarkia.py b/cgi/anarkia.py
new file mode 100644
index 0000000..6b7e5fd
--- /dev/null
+++ b/cgi/anarkia.py
@@ -0,0 +1,439 @@
+# coding=utf-8
+import _mysql
+from database import *
+from framework import *
+from template import *
+from img import *
+from post import *
+from settings import Settings
+
+d_thread = {}
+d_post = {}
+
+def anarkia(self, path_split):
+ setBoard('anarkia')
+
+ if len(path_split) <= 2:
+ self.output = main()
+ return
+
+ raise UserError, 'Ya fue, baisano...'
+
+ if path_split[2] == 'opt':
+ self.output = boardoptions(self.formdata)
+ elif path_split[2] == 'mod':
+ self.output = mod(self.formdata)
+ elif path_split[2] == 'bans':
+ self.output = bans(self.formdata)
+ elif path_split[2] == 'css':
+ self.output = css(self.formdata)
+ elif path_split[2] == 'type':
+ self.output = type(self.formdata)
+ elif path_split[2] == 'emojis':
+ self.output = emojis(self.formdata)
+ else:
+ raise UserError, 'ke?'
+
+def main():
+ board = Settings._.BOARD
+
+ logs = FetchAll("SELECT * FROM `logs` WHERE `staff` = 'Anarko' ORDER BY `timestamp` DESC")
+ for log in logs:
+ log['timestamp_formatted'] = formatTimestamp(log['timestamp'])
+
+ return renderTemplate('anarkia.html', {'mode': 0, 'logs': logs})
+
+def type(formdata):
+ board = Settings._.BOARD
+
+ if board['board_type'] == '1':
+ (type_now, type_do, do_num) = ('BBS', 'IB', '0')
+ else:
+ (type_now, type_do, do_num) = ('IB', 'BBS', '1')
+
+ if formdata.get('transform') == 'do':
+ t = 0
+ try:
+ with open('anarkia_time') as f:
+ t = int(f.read())
+ except IOError:
+ pass
+
+ dif = time.time() - t
+ if dif > (10 * 60):
+ #if True:
+ import re
+ t = time.time()
+
+ board['board_type'] = do_num
+ board['force_css'] = Settings.HOME_URL + 'anarkia/style_' + type_do.lower() + '.css'
+ updateBoardSettings()
+
+ # update posts
+ fix_board()
+
+ # regenerate
+ setBoard('anarkia')
+ regenerateBoard(True)
+
+ tf = timeTaken(t, time.time())
+
+ with open('anarkia_time', 'w') as f:
+ t = f.write(str(int(time.time())))
+
+ msg = 'Cambiada estructura de sección a %s. (%s)' % (type_do, tf)
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ else:
+ raise UserError, 'Esta acción sólo se puede realizar cada 10 minutos. Faltan: %d mins.' % (10-int(dif/60))
+
+ return renderTemplate('anarkia.html', {'mode': 7, 'type_now': type_now, 'type_do': type_do})
+
+def fix_board():
+ board = Settings._.BOARD
+ get_fix_dictionary()
+
+ if board['board_type'] == '1':
+ to_fix = FetchAll("SELECT * FROM posts WHERE message LIKE '%%anarkia/res/%%' AND boardid = %s" % board['id'])
+ else:
+ to_fix = FetchAll("SELECT * FROM posts WHERE message LIKE '%%anarkia/read/%%' AND boardid = %s" % board['id'])
+
+ for p in to_fix:
+ try:
+ if board['board_type'] == '1':
+ newmessage = re.sub(r'/anarkia/res/(\d+).html#(\d+)">&gt;&gt;(\d+)', fix_to_bbs, p['message'])
+ else:
+ newmessage = re.sub(r'/anarkia/read/(\d+)/(\d+)">&gt;&gt;(\d+)', fix_to_ib, p['message'])
+
+ UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % \
+ (_mysql.escape_string(newmessage), board['id'], p['id']))
+ except KeyError:
+ pass
+
+ return True
+
+def fix_to_bbs(matchobj):
+ threadid = matchobj.group(1)
+ pid = matchobj.group(2)
+ new_thread = d_thread[threadid]
+ new_post = d_post[new_thread][pid]
+ return '/anarkia/read/%s/%s">&gt;&gt;%s' % (new_thread, new_post, new_post)
+
+def fix_to_ib(matchobj):
+ threadid = matchobj.group(1)
+ num = int(matchobj.group(2))
+ new_thread = d_thread[threadid]
+ new_post = d_post[new_thread][num]
+ return '/anarkia/res/%s.html#%s">&gt;&gt;%s' % (new_thread, new_post, new_post)
+
+def get_fix_dictionary():
+ global d_thread, d_post
+ board = Settings._.BOARD
+ res = FetchAll("SELECT id, timestamp, parentid FROM posts WHERE boardid = %s ORDER BY CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC" % board['id'])
+ num = 1
+ thread = 0
+ for p in res:
+ pid = p['id']
+ if p['parentid'] == '0':
+ num = 1
+
+ time = p['timestamp']
+ if board['board_type'] == '1':
+ d_thread[pid] = time
+ thread = time
+ else:
+ d_thread[time] = pid
+ thread = pid
+
+ d_post[thread] = {}
+
+ if board['board_type'] == '1':
+ d_post[thread][pid] = num
+ else:
+ d_post[thread][num] = pid
+ num += 1
+
+ return
+
+def css(formdata):
+ board = Settings._.BOARD
+
+ if board['board_type'] == '1':
+ basename = 'style_bbs.css'
+ else:
+ basename = 'style_ib.css'
+
+ fname = '%sanarkia/%s' % (Settings.HOME_DIR, basename)
+
+ if formdata.get('cssfile'):
+ with open(fname, 'w') as f:
+ cssfile = f.write(formdata['cssfile'])
+
+ msg = 'CSS actualizado.'
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+
+ with open(fname) as f:
+ cssfile = f.read()
+
+ return renderTemplate('anarkia.html', {'mode': 6, 'basename': basename, 'cssfile': cssfile})
+
+def bans(formdata):
+ board = Settings._.BOARD
+
+ if formdata.get('unban'):
+ unban = int(formdata['unban'])
+ boardpickle = pickle.dumps(['anarkia'])
+
+ ban = FetchOne("SELECT * FROM `bans` WHERE id = %d" % unban)
+ if not ban:
+ raise UserError, "Ban inválido."
+ if ban['boards'] != boardpickle:
+ raise USerError, "Ban inválido."
+
+ UpdateDb('DELETE FROM `bans` WHERE id = %s' % ban['id'])
+ logAction("Usuario %s desbaneado." % ban['ip'][:4])
+ regenerateAccess()
+
+ bans = FetchAll('SELECT * FROM `bans` WHERE staff = \'anarko\'')
+ for ban in bans:
+ ban['added'] = formatTimestamp(ban['added'])
+ if ban['until'] == '0':
+ ban['until'] = _('Does not expire')
+ else:
+ ban['until'] = formatTimestamp(ban['until'])
+ return renderTemplate('anarkia.html', {'mode': 5, 'bans': bans})
+
+def mod(formdata):
+ board = Settings._.BOARD
+
+ if formdata.get('thread'):
+ parentid = int(formdata['thread'])
+ posts = FetchAll('SELECT * FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % (parentid, parentid, board['id']))
+ return renderTemplate('anarkia.html', {'mode': 3, 'posts': posts})
+ elif formdata.get('lock'):
+ postid = int(formdata['lock'])
+ post = FetchOne('SELECT id, locked FROM posts WHERE boardid = %s AND id = %d AND parentid = 0 LIMIT 1' % (board['id'], postid))
+ if post['locked'] == '0':
+ setLocked = 1
+ msg = "Hilo %s cerrado." % post['id']
+ else:
+ setLocked = 0
+ msg = "Hilo %s abierto." % post['id']
+
+ UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setLocked, board["id"], post["id"]))
+ threadUpdated(post['id'])
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ elif formdata.get('del'):
+ postid = int(formdata['del'])
+ post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid))
+ if post['parentid'] != '0':
+ deletePost(post['id'], None, '3', False)
+ msg = "Mensaje %s eliminado." % post['id']
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ else:
+ raise UserError, "jaj no"
+ elif formdata.get('restore'):
+ postid = int(formdata['restore'])
+ post = FetchOne('SELECT id, parentid FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid))
+
+ UpdateDb('UPDATE `posts` SET `IS_DELETED` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1' % (board['id'], post['id']))
+ if post['parentid'] != '0':
+ threadUpdated(post['parentid'])
+ else:
+ regenerateFrontPages()
+ msg = "Mensaje %s recuperado." % post['id']
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ elif formdata.get('ban'):
+ postid = int(formdata['ban'])
+ post = FetchOne('SELECT id, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid))
+
+ return renderTemplate('anarkia.html', {'mode': 4, 'post': post})
+ elif formdata.get('banto'):
+ postid = int(formdata['banto'])
+ post = FetchOne('SELECT id, message, parentid, ip FROM posts WHERE boardid = %s AND id = %d LIMIT 1' % (board['id'], postid))
+
+ reason = formdata.get('reason').replace('script', '').replace('meta', '')
+ if reason is not None:
+ if formdata['seconds'] != '0':
+ until = str(timestamp() + int(formdata['seconds']))
+ else:
+ until = '0'
+ where = pickle.dumps(['anarkia'])
+
+ ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + post['ip'] + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1")
+ if ban:
+ raise UserError, "Este usuario ya esta baneado."
+
+ # Blind mode
+ if formdata.get('blind') == '1':
+ blind = '1'
+ else:
+ blind = '0'
+
+ InsertDb("INSERT INTO `bans` (`ip`, `netmask`, `boards`, `added`, `until`, `staff`, `reason`, `blind`) VALUES ('" + post['ip'] + "', INET_ATON('255.255.255.255'), '" + _mysql.escape_string(where) + "', " + str(timestamp()) + ", " + until + ", 'anarko', '" + _mysql.escape_string(formdata['reason']) + "', '"+blind+"')")
+
+ newmessage = post['message'] + '<hr /><span class="banned">A este usuario se le revocó el acceso. Razón: %s</span>' % reason
+
+ UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % (_mysql.escape_string(newmessage), board['id'], post['id']))
+ if post['parentid'] != '0':
+ threadUpdated(post['parentid'])
+ else:
+ regenerateFrontPages()
+ regenerateAccess()
+
+ msg = "Usuario %s baneado." % post['ip'][:4]
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ else:
+ reports = FetchAll("SELECT * FROM `reports` WHERE board = 'anarkia'")
+ threads = FetchAll('SELECT * FROM `posts` WHERE boardid = %s AND parentid = 0 ORDER BY `bumped` DESC' % board['id'])
+ return renderTemplate('anarkia.html', {'mode': 2, 'threads': threads, 'reports': reports})
+
+def boardoptions(formdata):
+ board = Settings._.BOARD
+
+ if formdata.get('longname'):
+ # submitted
+ board['longname'] = formdata['longname'].replace('script', '')
+ board['postarea_desc'] = formdata['postarea_desc'].replace('script', '').replace('meta', '')
+ board['postarea_extra'] = formdata['postarea_extra'].replace('script', '').replace('meta', '')
+ board['anonymous'] = formdata['anonymous'].replace('script', '')
+ board['subject'] = formdata['subject'].replace('script', '')
+ board['message'] = formdata['message'].replace('script', '')
+ board['useid'] = formdata['useid']
+ if 'disable_name' in formdata.keys():
+ board['disable_name'] = '1'
+ else:
+ board['disable_name'] = '0'
+ if 'disable_subject' in formdata.keys():
+ board['disable_subject'] = '1'
+ else:
+ board['disable_subject'] = '0'
+ if 'allow_noimage' in formdata.keys():
+ board['allow_noimage'] = '1'
+ else:
+ board['allow_noimage'] = '0'
+ if 'allow_images' in formdata.keys():
+ board['allow_images'] = '1'
+ else:
+ board['allow_images'] = '0'
+ if 'allow_image_replies' in formdata.keys():
+ board['allow_image_replies'] = '1'
+ else:
+ board['allow_image_replies'] = '0'
+
+ # Update file types
+ UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id'])
+ for filetype in filetypelist():
+ if 'filetype'+filetype['ext'] in formdata.keys():
+ UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % (board['id'], filetype['id']))
+
+ try:
+ board['maxsize'] = int(formdata['maxsize'])
+ if board['maxsize'] > 10000:
+ board['maxsize'] = 10000
+ except:
+ raise UserError, _("Max size must be numeric.")
+
+ try:
+ board['thumb_px'] = int(formdata['thumb_px'])
+ if board['thumb_px'] > 500:
+ board['thumb_px'] = 500
+ except:
+ raise UserError, _("Max thumb dimensions must be numeric.")
+
+ try:
+ board['numthreads'] = int(formdata['numthreads'])
+ if board['numthreads'] > 15:
+ board['numthreads'] = 15
+ except:
+ raise UserError, _("Max threads shown must be numeric.")
+
+ try:
+ board['numcont'] = int(formdata['numcont'])
+ if board['numcont'] > 15:
+ board['numcont'] = 15
+ except:
+ raise UserError, _("Max replies shown must be numeric.")
+
+ t = time.time()
+ updateBoardSettings()
+ setBoard('anarkia')
+ regenerateBoard(True)
+ tf = timeTaken(t, time.time())
+
+ msg = 'Opciones cambiadas. %s' % tf
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ else:
+ return renderTemplate('anarkia.html', {'mode': 1, 'boardopts': board, 'filetypes': filetypelist(), 'supported_filetypes': board['filetypes_ext']})
+
+def emojis(formdata):
+ board = Settings._.BOARD
+ board_pickle = _mysql.escape_string(pickle.dumps([board['dir']]))
+
+ if formdata.get('new'):
+ import re
+ ext = {'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/png': 'png'}
+
+ if not formdata['name']:
+ raise UserError, 'Ingresa nombre.'
+ if not re.match(r"^[0-9a-zA-Z]+$", formdata['name']):
+ raise UserError, 'Nombre inválido; solo letras/números.'
+
+ name = ":%s:" % formdata['name'][:15]
+ data = formdata['file']
+
+ if not data:
+ raise UserError, 'Ingresa imagen.'
+
+ # check if it exists
+ already = FetchOne("SELECT 1 FROM `filters` WHERE `boards` = '%s' AND `from` = '%s'" % (board_pickle, _mysql.escape_string(name)))
+ if already:
+ raise UserError, 'Este emoji ya existe.'
+
+ # get image information
+ content_type, width, height, size, extra = getImageInfo(data)
+
+ if content_type not in ext.keys():
+ raise UserError, 'Formato inválido.'
+ if width > 500 or height > 500:
+ raise UserError, 'Dimensiones muy altas.'
+ if size > 150000:
+ raise UserError, 'Tamaño muy grande.'
+
+ # create file names
+ thumb_width, thumb_height = getThumbDimensions(width, height, 32)
+
+ file_path = Settings.ROOT_DIR + board["dir"] + "/e/" + formdata['name'][:15] + '.' + ext[content_type]
+ file_url = Settings.BOARDS_URL + board["dir"] + "/e/" + formdata['name'][:15] + '.' + ext[content_type]
+ to_filter = '<img src="%s" width="%d" height="%d" />' % (file_url, thumb_width, thumb_height)
+
+ # start processing image
+ args = [Settings.CONVERT_PATH, "-", "-limit" , "thread", "1", "-resize", "%dx%d" % (thumb_width, thumb_height), "-quality", "80", file_path]
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
+ out = p.communicate(input=data)[0]
+
+ # insert into DB
+ sql = "INSERT INTO `filters` (`boards`, `type`, `action`, `from`, `to`, `staff`, `added`) VALUES ('%s', 0, 1, '%s', '%s', 'Anarko', '%s')" % (board_pickle, _mysql.escape_string(name), _mysql.escape_string(to_filter), timestamp())
+ UpdateDb(sql)
+
+ msg = "Emoji %s agregado." % name
+ logAction(msg)
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': msg})
+ elif formdata.get('del'):
+ return renderTemplate('anarkia.html', {'mode': 99, 'msg': 'Del.'})
+ else:
+ filters = FetchAll("SELECT * FROM `filters` WHERE `boards` = '%s' ORDER BY `added` DESC" % board_pickle)
+ return renderTemplate('anarkia.html', {'mode': 8, 'emojis': filters})
+
+def filetypelist():
+ filetypes = FetchAll('SELECT * FROM `filetypes` ORDER BY `ext` ASC')
+ return filetypes
+
+def logAction(action):
+ InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + ", 'Anarko', '" + _mysql.escape_string(action) + "')") \ No newline at end of file
diff --git a/cgi/api.py b/cgi/api.py
new file mode 100644
index 0000000..8960578
--- /dev/null
+++ b/cgi/api.py
@@ -0,0 +1,392 @@
+# coding=utf-8
+import json
+import _mysql
+import time
+
+from framework import *
+from database import *
+from post import *
+
+def api(self, path_split):
+ if len(path_split) > 2:
+ try:
+ self.output = api_process(self, path_split)
+ except APIError, e:
+ self.output = api_error("error", e.message)
+ except UserError, e:
+ self.output = api_error("failed", e.message)
+ except Exception, e:
+ import sys, traceback
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ detail = ["%s : %s : %s : %s" % (os.path.basename(o[0]),o[1],o[2],o[3]) for o in traceback.extract_tb(exc_traceback)]
+
+ self.output = api_error("exception", str(e), str(type(e)), detail)
+ else:
+ self.output = api_error("error", "No method specified")
+
+def api_process(self, path_split):
+ formdata = self.formdata
+ ip = self.environ["REMOTE_ADDR"]
+ t = time.time()
+ method = path_split[2]
+
+ #bans = ['181.72.116.62']
+ bans = []
+ if ip in bans:
+ raise APIError, "You have been blacklisted."
+
+ #with open('../api_log.txt', 'a') as f:
+ # logstr = "[%s] %s: %s\n" % (formatTimestamp(t), ip, repr(path_split))
+ # f.write(logstr)
+
+ values = {'state': 'success'}
+
+ if method == 'boards':
+ boards = FetchAll('SELECT dir, name, board_type, allow_images, allow_image_replies, maxsize FROM `boards` WHERE `secret`=0 ORDER BY `name` ASC')
+ values['boards'] = boards
+ for board in values['boards']:
+ board['board_type'] = int(board['board_type'])
+ board['allow_images'] = int(board['allow_images'])
+ board['allow_image_replies'] = int(board['allow_image_replies'])
+ board['maxsize'] = int(board['maxsize'])
+
+ elif method == 'last':
+ data_limit = formdata.get('limit')
+ data_since = formdata.get('since')
+
+ limit = 10
+ since = 0
+
+ if data_limit:
+ try:
+ limit = int(data_limit)
+ except ValueError:
+ raise APIError, "Limit must be numeric"
+
+ if data_since:
+ try:
+ since = int(data_since)
+ except ValueError:
+ raise APIError, "Since must be numeric"
+
+ if limit > 50:
+ raise APIError, "Maximum limit is 50"
+
+ sql = "SELECT posts.id, boards.dir, timestamp, timestamp_formatted, posts.name, tripcode, email, posts.subject, posts.message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, parentid FROM posts INNER JOIN boards ON boardid = boards.id WHERE timestamp > %d AND IS_DELETED = 0 AND email NOT LIKE '%%sage%%' AND boards.secret = 0 ORDER BY timestamp DESC LIMIT %d" % (since, limit)
+ values['posts'] = FetchAll(sql)
+
+ for post in values['posts']:
+ post['id'] = int(post['id'])
+ post['timestamp'] = int(post['timestamp'])
+ post['parentid'] = int(post['parentid'])
+ post['file_size'] = int(post['file_size'])
+ post['image_width'] = int(post['image_width'])
+ post['image_height'] = int(post['image_height'])
+ post['thumb_width'] = int(post['thumb_width'])
+ post['thumb_height'] = int(post['thumb_height'])
+ post['message'] = post['message'].decode('utf-8', 'replace')
+ elif method == 'lastage':
+ data_limit = formdata.get('limit')
+ data_time = formdata.get('time', 0)
+
+ limit = 30
+
+ if data_limit:
+ try:
+ limit = int(data_limit)
+ except ValueError:
+ raise APIError, "Limit must be numeric"
+
+ if limit > 50:
+ raise APIError, "Maximum limit is 50"
+
+ threads = getLastAge(limit)
+ if threads[0]['bumped'] > int(data_time):
+ values['threads'] = threads
+ else:
+ values['threads'] = []
+ elif method == 'list':
+ data_board = formdata.get('dir')
+ data_offset = formdata.get('offset')
+ data_limit = formdata.get('limit')
+ data_replies = formdata.get('replies')
+ offset = 0
+ limit = 10
+ numreplies = 2
+
+ if not data_board:
+ raise APIError, "Missing parameters"
+
+ if data_limit:
+ try:
+ limit = int(data_limit)
+ except ValueError:
+ raise APIError, "Limit must be numeric"
+
+ if data_offset:
+ try:
+ offset = int(data_offset)
+ except ValueError:
+ raise APIError, "Offset must be numeric"
+
+ if data_replies:
+ try:
+ numreplies = int(data_replies)
+ except ValueError:
+ raise APIError, "Replies must be numeric"
+
+ if data_replies and limit > 30:
+ raise APIError, "Maximum limit is 30"
+
+ board = setBoard(data_board)
+
+ #sql = "SELECT id, timestamp, bumped, timestamp_formatted, name, tripcode, email, subject, message, file, thumb FROM posts WHERE boardid = %s AND parentid = 0 AND IS_DELETED = 0 ORDER BY bumped DESC LIMIT %d" % (board['id'], limit)
+ sql = "SELECT p.id, p.timestamp, p.bumped, p.expires, p.expires_formatted, p.timestamp_formatted, p.name, p.tripcode, p.email, p.subject, p.message, p.file, p.file_size, p.image_width, p.image_height, p.thumb, p.thumb_height, p.thumb_width, p.locked, coalesce(x.count,0) AS total_replies, coalesce(x.files,0) AS total_files FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1) as count, count(nullif(file, '')) as files FROM `posts` WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s AND p.IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)d OFFSET %(offset)d" % {'board': board["id"], 'limit': limit, 'offset': offset}
+
+ threads = FetchAll(sql)
+
+ if numreplies:
+ for thread in threads:
+ lastreplies = FetchAll("SELECT id, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, IS_DELETED FROM `posts` WHERE parentid = %s AND boardid = %s ORDER BY `timestamp` DESC LIMIT %d" % (thread['id'], board['id'], numreplies))
+ lastreplies = lastreplies[::-1]
+ thread['id'] = int(thread['id'])
+ thread['timestamp'] = int(thread['timestamp'])
+ thread['bumped'] = int(thread['bumped'])
+ thread['expires'] = int(thread['expires'])
+ thread['total_replies'] = int(thread['total_replies'])
+ thread['total_files'] = int(thread['total_files'])
+ thread['file_size'] = int(thread['file_size'])
+ thread['image_width'] = int(thread['image_width'])
+ thread['image_height'] = int(thread['image_height'])
+ thread['thumb_width'] = int(thread['thumb_width'])
+ thread['thumb_height'] = int(thread['thumb_height'])
+ thread['locked'] = int(thread['locked'])
+
+ thread['replies'] = []
+
+ for post in lastreplies:
+ post['IS_DELETED'] = int(post['IS_DELETED'])
+ post['id'] = int(post['id'])
+ post['timestamp'] = int(post['timestamp'])
+
+ if post['IS_DELETED']:
+ empty_post = {'id': post['id'],
+ 'IS_DELETED': post['IS_DELETED'],
+ 'timestamp': post['timestamp'],
+ }
+ thread['replies'].append(empty_post)
+ else:
+ post['file_size'] = int(post['file_size'])
+ post['image_width'] = int(post['image_width'])
+ post['image_height'] = int(post['image_height'])
+ post['thumb_width'] = int(post['thumb_width'])
+ post['thumb_height'] = int(post['thumb_height'])
+ post['message'] = post['message'].decode('utf-8', 'replace')
+
+ thread['replies'].append(post)
+
+ values['threads'] = threads
+ elif method == 'thread':
+ data_board = formdata.get('dir')
+ data_threadid = formdata.get('id')
+ data_threadts = formdata.get('ts')
+ data_offset = formdata.get('offset')
+ data_limit = formdata.get('limit')
+ data_striphtml = formdata.get('nohtml')
+ striphtml = False
+ offset = 0
+ limit = 1000
+
+ if not data_board or (not data_threadid and not data_threadts):
+ raise APIError, "Missing parameters"
+
+ if data_limit:
+ try:
+ limit = int(data_limit)
+ except ValueError:
+ raise APIError, "Limit must be numeric"
+
+ if data_offset:
+ try:
+ offset = int(data_offset)
+ except ValueError:
+ raise APIError, "Offset must be numeric"
+
+ if data_striphtml:
+ if int(data_striphtml) == 1:
+ striphtml = True
+
+ board = setBoard(data_board)
+ search_field = 'id'
+ search_val = 0
+
+ try:
+ search_val = int(data_threadid)
+ except (ValueError, TypeError):
+ pass
+
+ try:
+ search_val = int(data_threadts)
+ search_field = 'timestamp'
+ except (ValueError, TypeError):
+ pass
+
+ if not search_val:
+ raise APIError, "No thread ID"
+
+ op_post = FetchOne("SELECT id, timestamp, subject, locked FROM posts WHERE `%s` = '%d' AND boardid = '%s' AND parentid = 0" % (search_field, search_val, board["id"]))
+
+ if not op_post:
+ raise APIError, "Not a thread"
+
+ values['id'] = int(op_post['id'])
+ values['timestamp'] = int(op_post['timestamp'])
+ values['subject'] = op_post['subject']
+ values['locked'] = int(op_post['locked'])
+
+ total_replies = int(FetchOne("SELECT COUNT(1) FROM posts WHERE boardid = '%s' AND parentid = '%d'" % (board["id"], values['id']), 0)[0])
+
+ values['total_replies'] = total_replies
+
+ sql = "SELECT id, parentid, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_width, image_height, thumb, thumb_width, thumb_height, IS_DELETED FROM posts WHERE boardid = %s AND (parentid = %s OR id = %s) ORDER BY id ASC LIMIT %d OFFSET %d" % (_mysql.escape_string(board['id']), values['id'], values['id'], limit, offset)
+ posts = FetchAll(sql)
+
+ values['posts'] = []
+
+ for post in posts:
+ post['IS_DELETED'] = int(post['IS_DELETED'])
+ post['id'] = int(post['id'])
+ post['parentid'] = int(post['parentid'])
+ post['timestamp'] = int(post['timestamp'])
+
+ if post['IS_DELETED']:
+ empty_post = {'id': post['id'],
+ 'IS_DELETED': post['IS_DELETED'],
+ 'parentid': post['parentid'],
+ 'timestamp': post['timestamp'],
+ }
+ values['posts'].append(empty_post)
+ else:
+ post['file_size'] = int(post['file_size'])
+ post['image_width'] = int(post['image_width'])
+ post['image_height'] = int(post['image_height'])
+ post['thumb_width'] = int(post['thumb_width'])
+ post['thumb_height'] = int(post['thumb_height'])
+ post['message'] = post['message'].decode('utf-8', 'replace')
+ if striphtml:
+ post['message'] = post['message'].replace("<br />", " ")
+ post['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub("", post['message'])
+ values['posts'].append(post)
+ elif method == 'get':
+ data_board = formdata.get('dir')
+ data_parentid = formdata.get('thread')
+ data_postid = formdata.get('id')
+ data_postnum = formdata.get('num')
+
+ if not data_board and (not data_postid or (not data_postnum and not data_parentid)):
+ raise APIError, "Missing parameters"
+
+ board = setBoard(data_board)
+ postid = 0
+
+ if data_postnum:
+ data_postid = getID(data_parentid, data_postid)
+
+ try:
+ postid = int(data_postid)
+ except ValueError:
+ raise APIError, "Post ID must be numeric"
+
+ post = FetchOne("SELECT id, parentid, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_width, image_height, thumb, thumb_width, thumb_height, IS_DELETED FROM posts WHERE `id`='%d' AND boardid='%s'" % (postid, board["id"]))
+
+ if not post:
+ raise APIError, "Post ID cannot be found"
+
+ values['posts'] = []
+
+ post['IS_DELETED'] = int(post['IS_DELETED'])
+ post['id'] = int(post['id'])
+ post['parentid'] = int(post['parentid'])
+ post['timestamp'] = int(post['timestamp'])
+
+ if post['IS_DELETED']:
+ empty_post = {'id': post['id'],
+ 'IS_DELETED': post['IS_DELETED'],
+ 'parentid': post['parentid'],
+ 'timestamp': post['timestamp'],
+ }
+ values['posts'].append(empty_post)
+ else:
+ post['file_size'] = int(post['file_size'])
+ post['image_width'] = int(post['image_width'])
+ post['image_height'] = int(post['image_height'])
+ post['thumb_width'] = int(post['thumb_width'])
+ post['thumb_height'] = int(post['thumb_height'])
+ post['message'] = post['message'].decode('utf-8', 'replace')
+ values['posts'].append(post)
+ elif method == 'delete':
+ data_board = formdata.get('dir')
+ data_postid = formdata.get('id')
+ data_imageonly = formdata.get('imageonly')
+ data_password = formdata.get('password')
+
+ if not data_board or not data_postid or not data_password:
+ raise APIError, "Missing parameters"
+
+ imageonly = False
+ board = setBoard(data_board)
+
+ try:
+ postid = int(data_postid)
+ except ValueError:
+ raise APIError, "Post ID must be numeric"
+
+ if data_imageonly and data_imageonly == 1:
+ imageonly = True
+
+ deletePost(postid, data_password, board['recyclebin'], imageonly)
+ elif method == 'post':
+ boarddir = formdata.get('board')
+
+ if not boarddir:
+ raise APIError, "Missing parameters"
+
+ parent = formdata.get('parent')
+ trap1 = formdata.get('name', '')
+ trap2 = formdata.get('email', '')
+ name = formdata.get('fielda', '')
+ email = formdata.get('fieldb', '')
+ subject = formdata.get('subject', '')
+ message = formdata.get('message', '')
+ file = formdata.get('file')
+ file_original = formdata.get('file_original')
+ spoil = formdata.get('spoil')
+ oek_file = formdata.get('oek_file')
+ password = formdata.get('password', '')
+ noimage = formdata.get('noimage')
+ mobile = ("mobile" in formdata.keys())
+
+ # call post function
+ (post_url, ttaken) = self.make_post(ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile)
+
+ values['post_url'] = post_url
+ values['time_taken'] = ttaken
+ else:
+ raise APIError, "Invalid method"
+
+ values['time'] = int(t)
+ #values['time_taken'] = time.time() - t
+ return json.dumps(values, sort_keys=True, separators=(',',':'))
+
+def api_error(errtype, msg, type=None, detail=None):
+ values = {'state': errtype, 'message': msg}
+
+ if type:
+ values['type'] = type
+ if detail:
+ values['detail'] = detail
+
+ return json.dumps(values)
+
+class APIError(Exception):
+ pass
diff --git a/cgi/database.py b/cgi/database.py
new file mode 100644
index 0000000..c8611c5
--- /dev/null
+++ b/cgi/database.py
@@ -0,0 +1,69 @@
+# coding=utf-8
+
+import threading
+import _mysql
+from settings import Settings
+
+database_lock = threading.Lock()
+
+try:
+ # Although SQLAlchemy is optional, it is highly recommended
+ import sqlalchemy.pool as pool
+ _mysql = pool.manage( module = _mysql,
+ pool_size = Settings.DATABASE_POOL_SIZE,
+ max_overflow = Settings.DATABASE_POOL_OVERFLOW)
+ Settings._.USING_SQLALCHEMY = True
+except ImportError:
+ pass
+
+def OpenDb():
+ if Settings._.CONN is None:
+ Settings._.CONN = _mysql.connect(host = Settings.DATABASE_HOST,
+ user = Settings.DATABASE_USERNAME,
+ passwd = Settings.DATABASE_PASSWORD,
+ db = Settings.DATABASE_DB)
+
+def FetchAll(query, method=1):
+ """
+ Query and fetch all results as a list
+ """
+ db = Settings._.CONN
+
+ db.query(query)
+ r = db.use_result()
+ return r.fetch_row(0, method)
+
+def FetchOne(query, method=1):
+ """
+ Query and fetch only the first result
+ """
+ db = Settings._.CONN
+
+ db.query(query)
+ r = db.use_result()
+ try:
+ return r.fetch_row(1, method)[0]
+ except:
+ return None
+
+def UpdateDb(query):
+ """
+ Update the DB (UPDATE/DELETE) and return # of affected rows
+ """
+ db = Settings._.CONN
+
+ db.query(query)
+ return db.affected_rows()
+
+def InsertDb(query):
+ """
+ Insert into the DB and return the primary key of new row
+ """
+ db = Settings._.CONN
+
+ db.query(query)
+ return db.insert_id()
+
+def CloseDb():
+ if Settings._.CONN is not None:
+ Settings._.CONN.close()
diff --git a/cgi/fcgi.py b/cgi/fcgi.py
new file mode 100644
index 0000000..8677679
--- /dev/null
+++ b/cgi/fcgi.py
@@ -0,0 +1,1332 @@
+# Copyright (c) 2002, 2003, 2005, 2006 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+"""
+fcgi - a FastCGI/WSGI gateway.
+
+For more information about FastCGI, see <http://www.fastcgi.com/>.
+
+For more information about the Web Server Gateway Interface, see
+<http://www.python.org/peps/pep-0333.html>.
+
+Example usage:
+
+ #!/usr/bin/env python
+ from myapplication import app # Assume app is your WSGI application object
+ from fcgi import WSGIServer
+ WSGIServer(app).run()
+
+See the documentation for WSGIServer/Server for more information.
+
+On most platforms, fcgi will fallback to regular CGI behavior if run in a
+non-FastCGI context. If you want to force CGI behavior, set the environment
+variable FCGI_FORCE_CGI to "Y" or "y".
+"""
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+import sys
+import os
+import signal
+import struct
+import cStringIO as StringIO
+import select
+import socket
+import errno
+import traceback
+
+try:
+ import thread
+ import threading
+ thread_available = True
+except ImportError:
+ import dummy_thread as thread
+ import dummy_threading as threading
+ thread_available = False
+
+# Apparently 2.3 doesn't define SHUT_WR? Assume it is 1 in this case.
+if not hasattr(socket, 'SHUT_WR'):
+ socket.SHUT_WR = 1
+
+__all__ = ['WSGIServer']
+
+# Constants from the spec.
+FCGI_LISTENSOCK_FILENO = 0
+
+FCGI_HEADER_LEN = 8
+
+FCGI_VERSION_1 = 1
+
+FCGI_BEGIN_REQUEST = 1
+FCGI_ABORT_REQUEST = 2
+FCGI_END_REQUEST = 3
+FCGI_PARAMS = 4
+FCGI_STDIN = 5
+FCGI_STDOUT = 6
+FCGI_STDERR = 7
+FCGI_DATA = 8
+FCGI_GET_VALUES = 9
+FCGI_GET_VALUES_RESULT = 10
+FCGI_UNKNOWN_TYPE = 11
+FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
+
+FCGI_NULL_REQUEST_ID = 0
+
+FCGI_KEEP_CONN = 1
+
+FCGI_RESPONDER = 1
+FCGI_AUTHORIZER = 2
+FCGI_FILTER = 3
+
+FCGI_REQUEST_COMPLETE = 0
+FCGI_CANT_MPX_CONN = 1
+FCGI_OVERLOADED = 2
+FCGI_UNKNOWN_ROLE = 3
+
+FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
+FCGI_MAX_REQS = 'FCGI_MAX_REQS'
+FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
+
+FCGI_Header = '!BBHHBx'
+FCGI_BeginRequestBody = '!HB5x'
+FCGI_EndRequestBody = '!LB3x'
+FCGI_UnknownTypeBody = '!B7x'
+
+FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
+FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
+
+if __debug__:
+ import time
+
+ # Set non-zero to write debug output to a file.
+ DEBUG = 0
+ DEBUGLOG = '/tmp/fcgi.log'
+
+ def _debug(level, msg):
+ if DEBUG < level:
+ return
+
+ try:
+ f = open(DEBUGLOG, 'a')
+ f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
+ f.close()
+ except:
+ pass
+
+class InputStream(object):
+ """
+ File-like object representing FastCGI input streams (FCGI_STDIN and
+ FCGI_DATA). Supports the minimum methods required by WSGI spec.
+ """
+ def __init__(self, conn):
+ self._conn = conn
+
+ # See Server.
+ self._shrinkThreshold = conn.server.inputStreamShrinkThreshold
+
+ self._buf = ''
+ self._bufList = []
+ self._pos = 0 # Current read position.
+ self._avail = 0 # Number of bytes currently available.
+
+ self._eof = False # True when server has sent EOF notification.
+
+ def _shrinkBuffer(self):
+ """Gets rid of already read data (since we can't rewind)."""
+ if self._pos >= self._shrinkThreshold:
+ self._buf = self._buf[self._pos:]
+ self._avail -= self._pos
+ self._pos = 0
+
+ assert self._avail >= 0
+
+ def _waitForData(self):
+ """Waits for more data to become available."""
+ self._conn.process_input()
+
+ def read(self, n=-1):
+ if self._pos == self._avail and self._eof:
+ return ''
+ while True:
+ if n < 0 or (self._avail - self._pos) < n:
+ # Not enough data available.
+ if self._eof:
+ # And there's no more coming.
+ newPos = self._avail
+ break
+ else:
+ # Wait for more data.
+ self._waitForData()
+ continue
+ else:
+ newPos = self._pos + n
+ break
+ # Merge buffer list, if necessary.
+ if self._bufList:
+ self._buf += ''.join(self._bufList)
+ self._bufList = []
+ r = self._buf[self._pos:newPos]
+ self._pos = newPos
+ self._shrinkBuffer()
+ return r
+
+ def readline(self, length=None):
+ if self._pos == self._avail and self._eof:
+ return ''
+ while True:
+ # Unfortunately, we need to merge the buffer list early.
+ if self._bufList:
+ self._buf += ''.join(self._bufList)
+ self._bufList = []
+ # Find newline.
+ i = self._buf.find('\n', self._pos)
+ if i < 0:
+ # Not found?
+ if self._eof:
+ # No more data coming.
+ newPos = self._avail
+ break
+ else:
+ # Wait for more to come.
+ self._waitForData()
+ continue
+ else:
+ newPos = i + 1
+ break
+ if length is not None:
+ if self._pos + length < newPos:
+ newPos = self._pos + length
+ r = self._buf[self._pos:newPos]
+ self._pos = newPos
+ self._shrinkBuffer()
+ return r
+
+ def readlines(self, sizehint=0):
+ total = 0
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline()
+ return lines
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ r = self.readline()
+ if not r:
+ raise StopIteration
+ return r
+
+ def add_data(self, data):
+ if not data:
+ self._eof = True
+ else:
+ self._bufList.append(data)
+ self._avail += len(data)
+
+class MultiplexedInputStream(InputStream):
+ """
+ A version of InputStream meant to be used with MultiplexedConnections.
+ Assumes the MultiplexedConnection (the producer) and the Request
+ (the consumer) are running in different threads.
+ """
+ def __init__(self, conn):
+ super(MultiplexedInputStream, self).__init__(conn)
+
+ # Arbitrates access to this InputStream (it's used simultaneously
+ # by a Request and its owning Connection object).
+ lock = threading.RLock()
+
+ # Notifies Request thread that there is new data available.
+ self._lock = threading.Condition(lock)
+
+ def _waitForData(self):
+ # Wait for notification from add_data().
+ self._lock.wait()
+
+ def read(self, n=-1):
+ self._lock.acquire()
+ try:
+ return super(MultiplexedInputStream, self).read(n)
+ finally:
+ self._lock.release()
+
+ def readline(self, length=None):
+ self._lock.acquire()
+ try:
+ return super(MultiplexedInputStream, self).readline(length)
+ finally:
+ self._lock.release()
+
+ def add_data(self, data):
+ self._lock.acquire()
+ try:
+ super(MultiplexedInputStream, self).add_data(data)
+ self._lock.notify()
+ finally:
+ self._lock.release()
+
+class OutputStream(object):
+ """
+ FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to
+ write() or writelines() immediately result in Records being sent back
+ to the server. Buffering should be done in a higher level!
+ """
+ def __init__(self, conn, req, type, buffered=False):
+ self._conn = conn
+ self._req = req
+ self._type = type
+ self._buffered = buffered
+ self._bufList = [] # Used if buffered is True
+ self.dataWritten = False
+ self.closed = False
+
+ def _write(self, data):
+ length = len(data)
+ while length:
+ toWrite = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN)
+
+ rec = Record(self._type, self._req.requestId)
+ rec.contentLength = toWrite
+ rec.contentData = data[:toWrite]
+ self._conn.writeRecord(rec)
+
+ data = data[toWrite:]
+ length -= toWrite
+
+ def write(self, data):
+ assert not self.closed
+
+ if not data:
+ return
+
+ self.dataWritten = True
+
+ if self._buffered:
+ self._bufList.append(data)
+ else:
+ self._write(data)
+
+ def writelines(self, lines):
+ assert not self.closed
+
+ for line in lines:
+ self.write(line)
+
+ def flush(self):
+ # Only need to flush if this OutputStream is actually buffered.
+ if self._buffered:
+ data = ''.join(self._bufList)
+ self._bufList = []
+ self._write(data)
+
+ # Though available, the following should NOT be called by WSGI apps.
+ def close(self):
+ """Sends end-of-stream notification, if necessary."""
+ if not self.closed and self.dataWritten:
+ self.flush()
+ rec = Record(self._type, self._req.requestId)
+ self._conn.writeRecord(rec)
+ self.closed = True
+
+class TeeOutputStream(object):
+ """
+ Simple wrapper around two or more output file-like objects that copies
+ written data to all streams.
+ """
+ def __init__(self, streamList):
+ self._streamList = streamList
+
+ def write(self, data):
+ for f in self._streamList:
+ f.write(data)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def flush(self):
+ for f in self._streamList:
+ f.flush()
+
+class StdoutWrapper(object):
+ """
+ Wrapper for sys.stdout so we know if data has actually been written.
+ """
+ def __init__(self, stdout):
+ self._file = stdout
+ self.dataWritten = False
+
+ def write(self, data):
+ if data:
+ self.dataWritten = True
+ self._file.write(data)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def __getattr__(self, name):
+ return getattr(self._file, name)
+
+def decode_pair(s, pos=0):
+ """
+ Decodes a name/value pair.
+
+ The number of bytes decoded as well as the name/value pair
+ are returned.
+ """
+ nameLength = ord(s[pos])
+ if nameLength & 128:
+ nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
+ pos += 4
+ else:
+ pos += 1
+
+ valueLength = ord(s[pos])
+ if valueLength & 128:
+ valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
+ pos += 4
+ else:
+ pos += 1
+
+ name = s[pos:pos+nameLength]
+ pos += nameLength
+ value = s[pos:pos+valueLength]
+ pos += valueLength
+
+ return (pos, (name, value))
+
+def encode_pair(name, value):
+ """
+ Encodes a name/value pair.
+
+ The encoded string is returned.
+ """
+ nameLength = len(name)
+ if nameLength < 128:
+ s = chr(nameLength)
+ else:
+ s = struct.pack('!L', nameLength | 0x80000000L)
+
+ valueLength = len(value)
+ if valueLength < 128:
+ s += chr(valueLength)
+ else:
+ s += struct.pack('!L', valueLength | 0x80000000L)
+
+ return s + name + value
+
+class Record(object):
+ """
+ A FastCGI Record.
+
+ Used for encoding/decoding records.
+ """
+ def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
+ self.version = FCGI_VERSION_1
+ self.type = type
+ self.requestId = requestId
+ self.contentLength = 0
+ self.paddingLength = 0
+ self.contentData = ''
+
+ def _recvall(sock, length):
+ """
+ Attempts to receive length bytes from a socket, blocking if necessary.
+ (Socket may be blocking or non-blocking.)
+ """
+ dataList = []
+ recvLen = 0
+ while length:
+ try:
+ data = sock.recv(length)
+ except socket.error, e:
+ if e[0] == errno.EAGAIN:
+ select.select([sock], [], [])
+ continue
+ else:
+ raise
+ if not data: # EOF
+ break
+ dataList.append(data)
+ dataLen = len(data)
+ recvLen += dataLen
+ length -= dataLen
+ return ''.join(dataList), recvLen
+ _recvall = staticmethod(_recvall)
+
+ def read(self, sock):
+ """Read and decode a Record from a socket."""
+ try:
+ header, length = self._recvall(sock, FCGI_HEADER_LEN)
+ except:
+ raise EOFError
+
+ if length < FCGI_HEADER_LEN:
+ raise EOFError
+
+ self.version, self.type, self.requestId, self.contentLength, \
+ self.paddingLength = struct.unpack(FCGI_Header, header)
+
+ if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
+ 'contentLength = %d' %
+ (sock.fileno(), self.type, self.requestId,
+ self.contentLength))
+
+ if self.contentLength:
+ try:
+ self.contentData, length = self._recvall(sock,
+ self.contentLength)
+ except:
+ raise EOFError
+
+ if length < self.contentLength:
+ raise EOFError
+
+ if self.paddingLength:
+ try:
+ self._recvall(sock, self.paddingLength)
+ except:
+ raise EOFError
+
+ def _sendall(sock, data):
+ """
+ Writes data to a socket and does not return until all the data is sent.
+ """
+ length = len(data)
+ while length:
+ try:
+ sent = sock.send(data)
+ except socket.error, e:
+ if e[0] == errno.EAGAIN:
+ select.select([], [sock], [])
+ continue
+ else:
+ raise
+ data = data[sent:]
+ length -= sent
+ _sendall = staticmethod(_sendall)
+
+ def write(self, sock):
+ """Encode and write a Record to a socket."""
+ self.paddingLength = -self.contentLength & 7
+
+ if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
+ 'contentLength = %d' %
+ (sock.fileno(), self.type, self.requestId,
+ self.contentLength))
+
+ header = struct.pack(FCGI_Header, self.version, self.type,
+ self.requestId, self.contentLength,
+ self.paddingLength)
+ self._sendall(sock, header)
+ if self.contentLength:
+ self._sendall(sock, self.contentData)
+ if self.paddingLength:
+ self._sendall(sock, '\x00'*self.paddingLength)
+
+class Request(object):
+ """
+ Represents a single FastCGI request.
+
+ These objects are passed to your handler and is the main interface
+ between your handler and the fcgi module. The methods should not
+ be called by your handler. However, server, params, stdin, stdout,
+ stderr, and data are free for your handler's use.
+ """
+ def __init__(self, conn, inputStreamClass):
+ self._conn = conn
+
+ self.server = conn.server
+ self.params = {}
+ self.stdin = inputStreamClass(conn)
+ self.stdout = OutputStream(conn, self, FCGI_STDOUT)
+ self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True)
+ self.data = inputStreamClass(conn)
+
+ def run(self):
+ """Runs the handler, flushes the streams, and ends the request."""
+ try:
+ protocolStatus, appStatus = self.server.handler(self)
+ except:
+ traceback.print_exc(file=self.stderr)
+ self.stderr.flush()
+ if not self.stdout.dataWritten:
+ self.server.error(self)
+
+ protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0
+
+ if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' %
+ (protocolStatus, appStatus))
+
+ self._flush()
+ self._end(appStatus, protocolStatus)
+
+ def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE):
+ self._conn.end_request(self, appStatus, protocolStatus)
+
+ def _flush(self):
+ self.stdout.close()
+ self.stderr.close()
+
+class CGIRequest(Request):
+ """A normal CGI request disguised as a FastCGI request."""
+ def __init__(self, server):
+ # These are normally filled in by Connection.
+ self.requestId = 1
+ self.role = FCGI_RESPONDER
+ self.flags = 0
+ self.aborted = False
+
+ self.server = server
+ self.params = dict(os.environ)
+ self.stdin = sys.stdin
+ self.stdout = StdoutWrapper(sys.stdout) # Oh, the humanity!
+ self.stderr = sys.stderr
+ self.data = StringIO.StringIO()
+
+ def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE):
+ sys.exit(appStatus)
+
+ def _flush(self):
+ # Not buffered, do nothing.
+ pass
+
+class Connection(object):
+ """
+ A Connection with the web server.
+
+ Each Connection is associated with a single socket (which is
+ connected to the web server) and is responsible for handling all
+ the FastCGI message processing for that socket.
+ """
+ _multiplexed = False
+ _inputStreamClass = InputStream
+
+ def __init__(self, sock, addr, server):
+ self._sock = sock
+ self._addr = addr
+ self.server = server
+
+ # Active Requests for this Connection, mapped by request ID.
+ self._requests = {}
+
+ def _cleanupSocket(self):
+ """Close the Connection's socket."""
+ try:
+ self._sock.shutdown(socket.SHUT_WR)
+ except:
+ return
+ try:
+ while True:
+ r, w, e = select.select([self._sock], [], [])
+ if not r or not self._sock.recv(1024):
+ break
+ except:
+ pass
+ self._sock.close()
+
+ def run(self):
+ """Begin processing data from the socket."""
+ self._keepGoing = True
+ while self._keepGoing:
+ try:
+ self.process_input()
+ except EOFError:
+ break
+ except (select.error, socket.error), e:
+ if e[0] == errno.EBADF: # Socket was closed by Request.
+ break
+ raise
+
+ self._cleanupSocket()
+
+ def process_input(self):
+ """Attempt to read a single Record from the socket and process it."""
+ # Currently, any children Request threads notify this Connection
+ # that it is no longer needed by closing the Connection's socket.
+ # We need to put a timeout on select, otherwise we might get
+ # stuck in it indefinitely... (I don't like this solution.)
+ while self._keepGoing:
+ try:
+ r, w, e = select.select([self._sock], [], [], 1.0)
+ except ValueError:
+ # Sigh. ValueError gets thrown sometimes when passing select
+ # a closed socket.
+ raise EOFError
+ if r: break
+ if not self._keepGoing:
+ return
+ rec = Record()
+ rec.read(self._sock)
+
+ if rec.type == FCGI_GET_VALUES:
+ self._do_get_values(rec)
+ elif rec.type == FCGI_BEGIN_REQUEST:
+ self._do_begin_request(rec)
+ elif rec.type == FCGI_ABORT_REQUEST:
+ self._do_abort_request(rec)
+ elif rec.type == FCGI_PARAMS:
+ self._do_params(rec)
+ elif rec.type == FCGI_STDIN:
+ self._do_stdin(rec)
+ elif rec.type == FCGI_DATA:
+ self._do_data(rec)
+ elif rec.requestId == FCGI_NULL_REQUEST_ID:
+ self._do_unknown_type(rec)
+ else:
+ # Need to complain about this.
+ pass
+
+ def writeRecord(self, rec):
+ """
+ Write a Record to the socket.
+ """
+ rec.write(self._sock)
+
+ def end_request(self, req, appStatus=0L,
+ protocolStatus=FCGI_REQUEST_COMPLETE, remove=True):
+ """
+ End a Request.
+
+ Called by Request objects. An FCGI_END_REQUEST Record is
+ sent to the web server. If the web server no longer requires
+ the connection, the socket is closed, thereby ending this
+ Connection (run() returns).
+ """
+ rec = Record(FCGI_END_REQUEST, req.requestId)
+ rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus,
+ protocolStatus)
+ rec.contentLength = FCGI_EndRequestBody_LEN
+ self.writeRecord(rec)
+
+ if remove:
+ del self._requests[req.requestId]
+
+ if __debug__: _debug(2, 'end_request: flags = %d' % req.flags)
+
+ if not (req.flags & FCGI_KEEP_CONN) and not self._requests:
+ self._cleanupSocket()
+ self._keepGoing = False
+
+ def _do_get_values(self, inrec):
+ """Handle an FCGI_GET_VALUES request from the web server."""
+ outrec = Record(FCGI_GET_VALUES_RESULT)
+
+ pos = 0
+ while pos < inrec.contentLength:
+ pos, (name, value) = decode_pair(inrec.contentData, pos)
+ cap = self.server.capability.get(name)
+ if cap is not None:
+ outrec.contentData += encode_pair(name, str(cap))
+
+ outrec.contentLength = len(outrec.contentData)
+ self.writeRecord(outrec)
+
+ def _do_begin_request(self, inrec):
+ """Handle an FCGI_BEGIN_REQUEST from the web server."""
+ role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData)
+
+ req = self.server.request_class(self, self._inputStreamClass)
+ req.requestId, req.role, req.flags = inrec.requestId, role, flags
+ req.aborted = False
+
+ if not self._multiplexed and self._requests:
+ # Can't multiplex requests.
+ self.end_request(req, 0L, FCGI_CANT_MPX_CONN, remove=False)
+ else:
+ self._requests[inrec.requestId] = req
+
+ def _do_abort_request(self, inrec):
+ """
+ Handle an FCGI_ABORT_REQUEST from the web server.
+
+ We just mark a flag in the associated Request.
+ """
+ req = self._requests.get(inrec.requestId)
+ if req is not None:
+ req.aborted = True
+
+ def _start_request(self, req):
+ """Run the request."""
+ # Not multiplexed, so run it inline.
+ req.run()
+
+ def _do_params(self, inrec):
+ """
+ Handle an FCGI_PARAMS Record.
+
+ If the last FCGI_PARAMS Record is received, start the request.
+ """
+ req = self._requests.get(inrec.requestId)
+ if req is not None:
+ if inrec.contentLength:
+ pos = 0
+ while pos < inrec.contentLength:
+ pos, (name, value) = decode_pair(inrec.contentData, pos)
+ req.params[name] = value
+ else:
+ self._start_request(req)
+
+ def _do_stdin(self, inrec):
+ """Handle the FCGI_STDIN stream."""
+ req = self._requests.get(inrec.requestId)
+ if req is not None:
+ req.stdin.add_data(inrec.contentData)
+
+ def _do_data(self, inrec):
+ """Handle the FCGI_DATA stream."""
+ req = self._requests.get(inrec.requestId)
+ if req is not None:
+ req.data.add_data(inrec.contentData)
+
+ def _do_unknown_type(self, inrec):
+ """Handle an unknown request type. Respond accordingly."""
+ outrec = Record(FCGI_UNKNOWN_TYPE)
+ outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type)
+ outrec.contentLength = FCGI_UnknownTypeBody_LEN
+ self.writeRecord(rec)
+
+class MultiplexedConnection(Connection):
+ """
+ A version of Connection capable of handling multiple requests
+ simultaneously.
+ """
+ _multiplexed = True
+ _inputStreamClass = MultiplexedInputStream
+
+ def __init__(self, sock, addr, server):
+ super(MultiplexedConnection, self).__init__(sock, addr, server)
+
+ # Used to arbitrate access to self._requests.
+ lock = threading.RLock()
+
+ # Notification is posted everytime a request completes, allowing us
+ # to quit cleanly.
+ self._lock = threading.Condition(lock)
+
+ def _cleanupSocket(self):
+ # Wait for any outstanding requests before closing the socket.
+ self._lock.acquire()
+ while self._requests:
+ self._lock.wait()
+ self._lock.release()
+
+ super(MultiplexedConnection, self)._cleanupSocket()
+
+ def writeRecord(self, rec):
+ # Must use locking to prevent intermingling of Records from different
+ # threads.
+ self._lock.acquire()
+ try:
+ # Probably faster than calling super. ;)
+ rec.write(self._sock)
+ finally:
+ self._lock.release()
+
+ def end_request(self, req, appStatus=0L,
+ protocolStatus=FCGI_REQUEST_COMPLETE, remove=True):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self).end_request(req, appStatus,
+ protocolStatus,
+ remove)
+ self._lock.notify()
+ finally:
+ self._lock.release()
+
+ def _do_begin_request(self, inrec):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self)._do_begin_request(inrec)
+ finally:
+ self._lock.release()
+
+ def _do_abort_request(self, inrec):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self)._do_abort_request(inrec)
+ finally:
+ self._lock.release()
+
+ def _start_request(self, req):
+ thread.start_new_thread(req.run, ())
+
+ def _do_params(self, inrec):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self)._do_params(inrec)
+ finally:
+ self._lock.release()
+
+ def _do_stdin(self, inrec):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self)._do_stdin(inrec)
+ finally:
+ self._lock.release()
+
+ def _do_data(self, inrec):
+ self._lock.acquire()
+ try:
+ super(MultiplexedConnection, self)._do_data(inrec)
+ finally:
+ self._lock.release()
+
+class Server(object):
+ """
+ The FastCGI server.
+
+ Waits for connections from the web server, processing each
+ request.
+
+ If run in a normal CGI context, it will instead instantiate a
+ CGIRequest and run the handler through there.
+ """
+ request_class = Request
+ cgirequest_class = CGIRequest
+
+ # Limits the size of the InputStream's string buffer to this size + the
+ # server's maximum Record size. Since the InputStream is not seekable,
+ # we throw away already-read data once this certain amount has been read.
+ inputStreamShrinkThreshold = 102400 - 8192
+
+ def __init__(self, handler=None, maxwrite=8192, bindAddress=None,
+ umask=None, multiplexed=False):
+ """
+ handler, if present, must reference a function or method that
+ takes one argument: a Request object. If handler is not
+ specified at creation time, Server *must* be subclassed.
+ (The handler method below is abstract.)
+
+ maxwrite is the maximum number of bytes (per Record) to write
+ to the server. I've noticed mod_fastcgi has a relatively small
+ receive buffer (8K or so).
+
+ bindAddress, if present, must either be a string or a 2-tuple. If
+ present, run() will open its own listening socket. You would use
+ this if you wanted to run your application as an 'external' FastCGI
+ app. (i.e. the webserver would no longer be responsible for starting
+ your app) If a string, it will be interpreted as a filename and a UNIX
+ socket will be opened. If a tuple, the first element, a string,
+ is the interface name/IP to bind to, and the second element (an int)
+ is the port number.
+
+ Set multiplexed to True if you want to handle multiple requests
+ per connection. Some FastCGI backends (namely mod_fastcgi) don't
+ multiplex requests at all, so by default this is off (which saves
+ on thread creation/locking overhead). If threads aren't available,
+ this keyword is ignored; it's not possible to multiplex requests
+ at all.
+ """
+ if handler is not None:
+ self.handler = handler
+ self.maxwrite = maxwrite
+ if thread_available:
+ try:
+ import resource
+ # Attempt to glean the maximum number of connections
+ # from the OS.
+ maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
+ except ImportError:
+ maxConns = 100 # Just some made up number.
+ maxReqs = maxConns
+ if multiplexed:
+ self._connectionClass = MultiplexedConnection
+ maxReqs *= 5 # Another made up number.
+ else:
+ self._connectionClass = Connection
+ self.capability = {
+ FCGI_MAX_CONNS: maxConns,
+ FCGI_MAX_REQS: maxReqs,
+ FCGI_MPXS_CONNS: multiplexed and 1 or 0
+ }
+ else:
+ self._connectionClass = Connection
+ self.capability = {
+ # If threads aren't available, these are pretty much correct.
+ FCGI_MAX_CONNS: 1,
+ FCGI_MAX_REQS: 1,
+ FCGI_MPXS_CONNS: 0
+ }
+ self._bindAddress = bindAddress
+ self._umask = umask
+
+ def _setupSocket(self):
+ if self._bindAddress is None: # Run as a normal FastCGI?
+ isFCGI = True
+
+ if isFCGI:
+ sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET,
+ socket.SOCK_STREAM)
+ try:
+ sock.getpeername()
+ except socket.error, e:
+ if e[0] == errno.ENOTSOCK:
+ # Not a socket, assume CGI context.
+ isFCGI = False
+ elif e[0] != errno.ENOTCONN:
+ raise
+
+ # FastCGI/CGI discrimination is broken on Mac OS X.
+ # Set the environment variable FCGI_FORCE_CGI to "Y" or "y"
+ # if you want to run your app as a simple CGI. (You can do
+ # this with Apache's mod_env [not loaded by default in OS X
+ # client, ha ha] and the SetEnv directive.)
+ if not isFCGI or \
+ os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'):
+ req = self.cgirequest_class(self)
+ req.run()
+ sys.exit(0)
+ else:
+ # Run as a server
+ oldUmask = None
+ if type(self._bindAddress) is str:
+ # Unix socket
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ os.unlink(self._bindAddress)
+ except OSError:
+ pass
+ if self._umask is not None:
+ oldUmask = os.umask(self._umask)
+ else:
+ # INET socket
+ assert type(self._bindAddress) is tuple
+ assert len(self._bindAddress) == 2
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ sock.bind(self._bindAddress)
+ sock.listen(socket.SOMAXCONN)
+
+ if oldUmask is not None:
+ os.umask(oldUmask)
+
+ return sock
+
+ def _cleanupSocket(self, sock):
+ """Closes the main socket."""
+ sock.close()
+
+ def _installSignalHandlers(self):
+ self._oldSIGs = [(x,signal.getsignal(x)) for x in
+ (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)]
+ signal.signal(signal.SIGHUP, self._hupHandler)
+ signal.signal(signal.SIGINT, self._intHandler)
+ signal.signal(signal.SIGTERM, self._intHandler)
+
+ def _restoreSignalHandlers(self):
+ for signum,handler in self._oldSIGs:
+ signal.signal(signum, handler)
+
+ def _hupHandler(self, signum, frame):
+ self._hupReceived = True
+ self._keepGoing = False
+
+ def _intHandler(self, signum, frame):
+ self._keepGoing = False
+
+ def run(self, timeout=1.0):
+ """
+ The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if
+ SIGHUP was received, False otherwise.
+ """
+ web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS')
+ if web_server_addrs is not None:
+ web_server_addrs = map(lambda x: x.strip(),
+ web_server_addrs.split(','))
+
+ sock = self._setupSocket()
+
+ self._keepGoing = True
+ self._hupReceived = False
+
+ # Install signal handlers.
+ self._installSignalHandlers()
+
+ while self._keepGoing:
+ try:
+ r, w, e = select.select([sock], [], [], timeout)
+ except select.error, e:
+ if e[0] == errno.EINTR:
+ continue
+ raise
+
+ if r:
+ try:
+ clientSock, addr = sock.accept()
+ except socket.error, e:
+ if e[0] in (errno.EINTR, errno.EAGAIN):
+ continue
+ raise
+
+ if web_server_addrs and \
+ (len(addr) != 2 or addr[0] not in web_server_addrs):
+ clientSock.close()
+ continue
+
+ # Instantiate a new Connection and begin processing FastCGI
+ # messages (either in a new thread or this thread).
+ conn = self._connectionClass(clientSock, addr, self)
+ thread.start_new_thread(conn.run, ())
+
+ self._mainloopPeriodic()
+
+ # Restore signal handlers.
+ self._restoreSignalHandlers()
+
+ self._cleanupSocket(sock)
+
+ return self._hupReceived
+
+ def _mainloopPeriodic(self):
+ """
+ Called with just about each iteration of the main loop. Meant to
+ be overridden.
+ """
+ pass
+
+ def _exit(self, reload=False):
+ """
+ Protected convenience method for subclasses to force an exit. Not
+ really thread-safe, which is why it isn't public.
+ """
+ if self._keepGoing:
+ self._keepGoing = False
+ self._hupReceived = reload
+
+ def handler(self, req):
+ """
+ Default handler, which just raises an exception. Unless a handler
+ is passed at initialization time, this must be implemented by
+ a subclass.
+ """
+ raise NotImplementedError, self.__class__.__name__ + '.handler'
+
+ def error(self, req):
+ """
+ Called by Request if an exception occurs within the handler. May and
+ should be overridden.
+ """
+ import cgitb
+ req.stdout.write('Content-Type: text/html\r\n\r\n' +
+ cgitb.html(sys.exc_info()))
+
+class WSGIServer(Server):
+ """
+ FastCGI server that supports the Web Server Gateway Interface. See
+ <http://www.python.org/peps/pep-0333.html>.
+ """
+ def __init__(self, application, environ=None, umask=None,
+ multithreaded=True, **kw):
+ """
+ environ, if present, must be a dictionary-like object. Its
+ contents will be copied into application's environ. Useful
+ for passing application-specific variables.
+
+ Set multithreaded to False if your application is not MT-safe.
+ """
+ if kw.has_key('handler'):
+ del kw['handler'] # Doesn't make sense to let this through
+ super(WSGIServer, self).__init__(**kw)
+
+ if environ is None:
+ environ = {}
+
+ self.application = application
+ self.environ = environ
+ self.multithreaded = multithreaded
+
+ # Used to force single-threadedness
+ self._app_lock = thread.allocate_lock()
+
+ def handler(self, req):
+ """Special handler for WSGI."""
+ if req.role != FCGI_RESPONDER:
+ return FCGI_UNKNOWN_ROLE, 0
+
+ # Mostly taken from example CGI gateway.
+ environ = req.params
+ environ.update(self.environ)
+
+ environ['wsgi.version'] = (1,0)
+ environ['wsgi.input'] = req.stdin
+ if self._bindAddress is None:
+ stderr = req.stderr
+ else:
+ stderr = TeeOutputStream((sys.stderr, req.stderr))
+ environ['wsgi.errors'] = stderr
+ environ['wsgi.multithread'] = not isinstance(req, CGIRequest) and \
+ thread_available and self.multithreaded
+ # Rationale for the following: If started by the web server
+ # (self._bindAddress is None) in either FastCGI or CGI mode, the
+ # possibility of being spawned multiple times simultaneously is quite
+ # real. And, if started as an external server, multiple copies may be
+ # spawned for load-balancing/redundancy. (Though I don't think
+ # mod_fastcgi supports this?)
+ environ['wsgi.multiprocess'] = True
+ environ['wsgi.run_once'] = isinstance(req, CGIRequest)
+
+ if environ.get('HTTPS', 'off') in ('on', '1'):
+ environ['wsgi.url_scheme'] = 'https'
+ else:
+ environ['wsgi.url_scheme'] = 'http'
+
+ self._sanitizeEnv(environ)
+
+ headers_set = []
+ headers_sent = []
+ result = None
+
+ def write(data):
+ assert type(data) is str, 'write() argument must be string'
+ assert headers_set, 'write() before start_response()'
+
+ if not headers_sent:
+ status, responseHeaders = headers_sent[:] = headers_set
+ found = False
+ for header,value in responseHeaders:
+ if header.lower() == 'content-length':
+ found = True
+ break
+ if not found and result is not None:
+ try:
+ if len(result) == 1:
+ responseHeaders.append(('Content-Length',
+ str(len(data))))
+ except:
+ pass
+ s = 'Status: %s\r\n' % status
+ for header in responseHeaders:
+ s += '%s: %s\r\n' % header
+ s += '\r\n'
+ req.stdout.write(s)
+
+ req.stdout.write(data)
+ req.stdout.flush()
+
+ def start_response(status, response_headers, exc_info=None):
+ if exc_info:
+ try:
+ if headers_sent:
+ # Re-raise if too late
+ raise exc_info[0], exc_info[1], exc_info[2]
+ finally:
+ exc_info = None # avoid dangling circular ref
+ else:
+ assert not headers_set, 'Headers already set!'
+
+ assert type(status) is str, 'Status must be a string'
+ assert len(status) >= 4, 'Status must be at least 4 characters'
+ assert int(status[:3]), 'Status must begin with 3-digit code'
+ assert status[3] == ' ', 'Status must have a space after code'
+ assert type(response_headers) is list, 'Headers must be a list'
+ if __debug__:
+ for name,val in response_headers:
+ assert type(name) is str, 'Header names must be strings'
+ assert type(val) is str, 'Header values must be strings'
+
+ headers_set[:] = [status, response_headers]
+ return write
+
+ if not self.multithreaded:
+ self._app_lock.acquire()
+ try:
+ try:
+ result = self.application(environ, start_response)
+ try:
+ for data in result:
+ if data:
+ write(data)
+ if not headers_sent:
+ write('') # in case body was empty
+ finally:
+ if hasattr(result, 'close'):
+ result.close()
+ except socket.error, e:
+ if e[0] != errno.EPIPE:
+ raise # Don't let EPIPE propagate beyond server
+ finally:
+ if not self.multithreaded:
+ self._app_lock.release()
+
+ return FCGI_REQUEST_COMPLETE, 0
+
+ def _sanitizeEnv(self, environ):
+ """Ensure certain values are present, if required by WSGI."""
+ if not environ.has_key('SCRIPT_NAME'):
+ environ['SCRIPT_NAME'] = ''
+ if not environ.has_key('PATH_INFO'):
+ environ['PATH_INFO'] = ''
+
+ # If any of these are missing, it probably signifies a broken
+ # server...
+ for name,default in [('REQUEST_METHOD', 'GET'),
+ ('SERVER_NAME', 'localhost'),
+ ('SERVER_PORT', '80'),
+ ('SERVER_PROTOCOL', 'HTTP/1.0')]:
+ if not environ.has_key(name):
+ environ['wsgi.errors'].write('%s: missing FastCGI param %s '
+ 'required by WSGI!\n' %
+ (self.__class__.__name__, name))
+ environ[name] = default
+
+if __name__ == '__main__':
+ def test_app(environ, start_response):
+ """Probably not the most efficient example."""
+ import cgi
+ start_response('200 OK', [('Content-Type', 'text/html')])
+ yield '<html><head><title>Hello World!</title></head>\n' \
+ '<body>\n' \
+ '<p>Hello World!</p>\n' \
+ '<table border="1">'
+ names = environ.keys()
+ names.sort()
+ for name in names:
+ yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
+ name, cgi.escape(`environ[name]`))
+
+ form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ,
+ keep_blank_values=1)
+ if form.list:
+ yield '<tr><th colspan="2">Form data</th></tr>'
+
+ for field in form.list:
+ yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
+ field.name, field.value)
+
+ yield '</table>\n' \
+ '</body></html>\n'
+
+ WSGIServer(test_app).run()
diff --git a/cgi/formatting.py b/cgi/formatting.py
new file mode 100644
index 0000000..d21bee2
--- /dev/null
+++ b/cgi/formatting.py
@@ -0,0 +1,425 @@
+# coding=utf-8
+import string
+import cgi
+import os
+import re
+import pickle
+import time
+import _mysql
+
+from database import *
+from framework import *
+from post import regenerateAccess
+#from xhtml_clean import Cleaner
+
+from settings import Settings
+
+def format_post(message, ip, parentid, parent_timestamp=0):
+ """
+ Formats posts using the specified format
+ """
+ board = Settings._.BOARD
+ using_markdown = False
+
+ # Escape any HTML if user is not using Markdown or HTML
+ if not Settings.USE_HTML:
+ message = cgi.escape(message)
+
+ # Strip text
+ message = message.rstrip()[0:8000]
+
+ # Treat HTML
+ if Settings.USE_MARKDOWN:
+ message = markdown(message)
+ using_markdown = True
+ if Settings.USE_HTML:
+ message = onlyAllowedHTML(message)
+
+ # [code] tag
+ if board["dir"] == "tech":
+ message = re.compile(r"\[code\](.+)\[/code\]", re.DOTALL | re.IGNORECASE).sub(r"<pre><code>\1</code></pre>", message)
+ if board["allow_spoilers"]:
+ message = re.compile(r"\[spoiler\](.+)\[/spoiler\]", re.DOTALL | re.IGNORECASE).sub(r'<span class="spoil">\1</span>', message)
+
+ if Settings.VIDEO_THUMBS:
+ (message, affected) = videoThumbs(message)
+ if affected:
+ message = close_html(message)
+
+ message = clickableURLs(message)
+ message = checkRefLinks(message, parentid, parent_timestamp)
+ message = checkWordfilters(message, ip, board["dir"])
+
+ # If not using markdown quotes must be created and \n changed for HTML line breaks
+ if not using_markdown:
+ message = re.compile(r"^(\n)+").sub('', message)
+ message = checkQuotes(message)
+ message = message.replace("\n", "<br />")
+
+ return message
+
+def tripcode(name):
+ """
+ Calculate tripcode to match output of most imageboards
+ """
+ if name == '':
+ return '', ''
+
+ board = Settings._.BOARD
+
+ name = name.decode('utf-8')
+ key = Settings.TRIP_CHAR.decode('utf-8')
+
+ # if there's a trip
+ (namepart, marker, trippart) = name.partition('#')
+ if marker:
+ namepart = cleanString(namepart)
+ trip = ''
+
+ # secure tripcode
+ if Settings.ALLOW_SECURE_TRIPCODES and '#' in trippart:
+ (trippart, securemarker, securepart) = trippart.partition('#')
+ try:
+ securepart = securepart.encode("sjis", "ignore")
+ except:
+ pass
+
+ # encode secure tripcode
+ trip = getMD5(securepart + Settings.SECRET)
+ trip = trip.encode('base64').replace('\n', '')
+ trip = trip.encode('rot13')
+ trip = key+key+trip[2:12]
+
+ # return it if we don't have a normal tripcode
+ if trippart == '':
+ return namepart.encode('utf-8'), trip.encode('utf-8')
+
+ # do normal tripcode
+ from crypt import crypt
+ try:
+ trippart = trippart.encode("sjis", "ignore")
+ except:
+ pass
+
+ trippart = cleanString(trippart, True, True)
+ salt = re.sub(r"[^\.-z]", ".", (trippart + "H..")[1:3])
+ salt = salt.translate(string.maketrans(r":;=?@[\]^_`", "ABDFGabcdef"))
+ trip = key + crypt(trippart, salt)[-10:] + trip
+
+ return namepart.encode('utf-8'), trip.encode('utf-8')
+
+ return name.encode('utf-8'), ''
+
+def iphash(ip, post, t, useid, mobile, agent, cap_id, hide_end, has_countrycode):
+ current_t = time.time()
+
+ if cap_id:
+ id = cap_id
+ elif 'sage' in post['email'] and useid == '1':
+ id = '???'
+ elif ip == "127.0.0.1":
+ id = '???'
+ else:
+ day = int((current_t + (Settings.TIME_ZONE*3600)) / 86400)
+ word = ',' + str(day)
+
+ # Make difference by thread
+ word += ',' + str(t)
+
+ id = hide_data(ip + word, 6, "id", Settings.SECRET)
+
+ if hide_end:
+ id += '*'
+ elif addressIsTor(ip):
+ id += 'T'
+ elif 'Dalvik' in agent:
+ id += 'R'
+ elif 'Android' in agent:
+ id += 'a'
+ elif 'iPhone' in agent:
+ id += 'i'
+ elif useid == '3':
+ if 'Firefox' in agent:
+ id += 'F'
+ elif 'Safari' in agent and not 'Chrome' in agent:
+ id += 's'
+ elif 'Chrome' in agent:
+ id += 'C'
+ elif 'SeaMonkey' in agent:
+ id += 'S'
+ elif 'Edge' in agent:
+ id += 'E'
+ elif 'Opera' in agent or 'OPR' in agent:
+ id += 'o'
+ elif 'MSIE' in agent or 'Trident' in agent:
+ id += 'I'
+ elif mobile:
+ id += 'Q'
+ else:
+ id += '0'
+ elif mobile:
+ id += 'Q'
+ else:
+ id += '0'
+
+ if addressIsBanned(ip, ""):
+ id += '#'
+ if (not has_countrycode and
+ not addressIsTor(ip) and
+ (addressIsProxy(ip) or not addressIsES(ip))):
+ id += '!'
+
+ return id
+
+def cleanString(string, escape=True, quote=False):
+ string = string.strip()
+ if escape:
+ string = cgi.escape(string, quote)
+ return string
+
+def clickableURLs(message):
+ # URL
+ message = re.compile(r'( |^|:|\(|\[)((?:https?://|ftp://|mailto:|news:|irc:)[^\s<>()"]*?(?:\([^\s<>()"]*?\)[^\s<>()"]*?)*)((?:\s|<|>|"|\.|\|\]|!|\?|,|&#44;|&quot;)*(?:[\s<>()"]|$))', re.M).sub(r'\1<a href="\2" rel="nofollow" target="_blank">\2</a>\3', message)
+ # Emails
+ message = re.compile(r"( |^|:)([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6})", re.I | re.M).sub(r'\1<a href="mailto:\2" rel="nofollow">&lt;\2&gt;</a>', message)
+
+ return message
+
+def videoThumbs(message):
+ # Youtube
+ __RE = re.compile(r"^(?: +)?(https?://(?:www\.)?youtu(?:be\.com/watch\?v=|\.be/)([\w\-]+))(?: +)?$", re.M)
+ matches = __RE.finditer(message)
+ if matches:
+ import json
+ import urllib, urllib2
+
+ v_ids = []
+ videos = {}
+
+ for match in matches:
+ v_id = match.group(2)
+ if v_id not in v_ids:
+ v_ids.append(v_id)
+ videos[v_id] = {
+ 'span': match.span(0),
+ 'url': match.group(1),
+ }
+ if len(v_ids) >= Settings.VIDEO_THUMBS_LIMIT:
+ raise UserError, "Has incluído muchos videos en tu mensaje. El máximo es %d." % Settings.VIDEO_THUMBS_LIMIT
+
+ if videos:
+ params = {
+ 'key': Settings.GOOGLE_API_KEY,
+ 'part': 'snippet,contentDetails',
+ 'id': ','.join(v_ids)
+ }
+ r_url = "https://www.googleapis.com/youtube/v3/videos?"+urllib.urlencode(params)
+ res = urllib2.urlopen(r_url)
+ res_json = json.load(res)
+
+ offset = 0
+ for item in res_json['items']:
+ v_id = item['id']
+ (start, end) = videos[v_id]['span']
+ end += 1 # remove endline
+
+ try:
+ new_url = '<a href="%(url)s" target="_blank" class="yt"><span class="pvw"><img src="%(thumb)s" /></span><b>%(title)s</b> (%(secs)s)<br />%(channel)s</a><br />' \
+ % {'title': item['snippet']['title'].encode('utf-8'),
+ 'channel': item['snippet']['channelTitle'].encode('utf-8'),
+ 'secs': parseIsoPeriod(item['contentDetails']['duration']).encode('utf-8'),
+ 'url': videos[v_id]['url'],
+ 'id': v_id.encode('utf-8'),
+ 'thumb': item['snippet']['thumbnails']['default']['url'].encode('utf-8'),}
+ except UnicodeDecodeError:
+ raise UserError, repr(v_id)
+ message = message[:start+offset] + new_url + message[end+offset:]
+ offset += len(new_url) - (end-start)
+
+ return (message, len(videos))
+
+def fixMobileLinks(message):
+ """
+ Shorten long links; Convert >># links into a mobile version
+ """
+ board = Settings._.BOARD
+
+ # If textboard
+ if board["board_type"] == '1':
+ message = re.compile(r'<a href="/(\w+)/read/(\d+)(\.html)?/*(.+)"').sub(r'<a href="/cgi/mobileread/\1/\2/\4"', message)
+ else:
+ message = re.compile(r'<a href="/(\w+)/res/(\d+)\.html#(\d+)"').sub(r'<a href="/cgi/mobileread/\1/\2#\3"', message)
+
+ return message
+
+def checkRefLinks(message, parentid, parent_timestamp):
+ """
+ Check for >># links in posts and replace with the HTML to make them clickable
+ """
+ board = Settings._.BOARD
+
+ if board["board_type"] == '1':
+ # Textboard
+ if parentid != '0':
+ message = re.compile(r'&gt;&gt;(\d+(,\d+|-(?=[ \d\n])|\d+)*n?)').sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/read/' + str(parent_timestamp) + r'/\1">&gt;&gt;\1</a>', message)
+ else:
+ # Imageboard
+ quotes_id_array = re.findall(r"&gt;&gt;([0-9]+)", message)
+ for quotes in quotes_id_array:
+ try:
+ post = FetchOne('SELECT * FROM `posts` WHERE `id` = ' + quotes + ' AND `boardid` = ' + board['id'] + ' LIMIT 1')
+ if post['parentid'] != '0':
+ message = re.compile("&gt;&gt;" + quotes).sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + post['parentid'] + '.html#' + quotes + '">&gt;&gt;' + quotes + '</a>', message)
+ else:
+ message = re.compile("&gt;&gt;" + quotes).sub('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + post['id'] + '.html#' + quotes + '">&gt;&gt;' + quotes + '</a>', message)
+ except:
+ message = re.compile("&gt;&gt;" + quotes).sub(r'<span class="q">&gt;&gt;'+quotes+'</span>', message)
+
+ return message
+
+def checkQuotes(message):
+ """
+ Check for >text in posts and add span around it to color according to the css
+ """
+ message = re.compile(r"^&gt;(.*)$", re.MULTILINE).sub(r'<span class="q">&gt;\1</span>', message)
+ return message
+
+def escapeHTML(string):
+ string = string.replace('<', '&lt;')
+ string = string.replace('>', '&gt;')
+ return string
+
+def onlyAllowedHTML(message):
+ """
+ Allow <b>, <i>, <u>, <strike>, and <pre> in posts, along with the special <aa>
+ """
+ message = sanitize_html(message)
+ #message = re.compile(r"\[aa\](.+?)\[/aa\]", re.DOTALL | re.IGNORECASE).sub("<span class=\"sjis\">\\1</span>", message)
+
+ return message
+
+def close_html(message):
+ """
+ Old retarded version of sanitize_html, it just closes open tags.
+ """
+ import BeautifulSoup
+ return unicode(BeautifulSoup.BeautifulSoup(message)).replace('&#13;', '').encode('utf-8')
+
+def sanitize_html(message, decode=True):
+ """
+ Clean the code and allow only a few safe tags.
+ """
+ import BeautifulSoup
+
+ # Decode message from utf-8 if required
+ if decode:
+ message = message.decode('utf-8', 'replace')
+
+ # Create HTML Cleaner with our allowed tags
+ whitelist_tags = ["a","b","br","blink","code","del","em","i","marquee","root","strike","strong","sub","sup","u"]
+ whitelist_attr = ["href"]
+
+ soup = BeautifulSoup.BeautifulSoup(message)
+
+ # Remove tags that aren't allowed
+ for tag in soup.findAll():
+ if not tag.name.lower() in whitelist_tags:
+ tag.name = "span"
+ tag.attrs = []
+ else:
+ for attr in [attr for attr in tag.attrs if attr not in whitelist_attr]:
+ del tag[attr]
+
+ # We export the soup into a correct XHTML string
+ string = unicode(soup).encode('utf-8')
+ # We remove some anomalies we don't want
+ string = string.replace('<br/>', '<br />').replace('&#13;', '')
+
+ return string
+
+def markdown(message):
+ import markdown
+ if message.strip() != "":
+ #return markdown.markdown(message).rstrip("\n").rstrip("<br />")
+ return markdown.markdown(message, extras=["cuddled-lists", "code-friendly"]).encode('utf-8')
+ else:
+ return ""
+
+def checkWordfilters(message, ip, board):
+ fixed_ip = inet_aton(ip)
+ wordfilters = FetchAll("SELECT * FROM `filters` WHERE `type` = '0' ORDER BY `id` ASC")
+ for wordfilter in wordfilters:
+ if wordfilter["boards"] != "":
+ boards = pickle.loads(wordfilter["boards"])
+ if wordfilter["boards"] == "" or board in boards:
+ if wordfilter['action'] == '0':
+ if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None:
+ raise UserError, wordfilter['reason']
+ elif wordfilter['action'] == '1':
+ message = re.compile(wordfilter['from'], re.DOTALL | re.IGNORECASE).sub(wordfilter['to'], message)
+ elif wordfilter['action'] == '2':
+ # Ban
+ if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None:
+ if wordfilter['seconds'] != '0':
+ until = str(timestamp() + int(wordfilter['seconds']))
+ else:
+ until = '0'
+
+ InsertDb("INSERT INTO `bans` (`ip`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (" + \
+ "'" + str(fixed_ip) + "', '" + _mysql.escape_string(wordfilter['boards']) + \
+ "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(wordfilter['reason']) + \
+ "', 'Word Auto-ban', '"+_mysql.escape_string(wordfilter['blind'])+"')")
+ regenerateAccess()
+ raise UserError, wordfilter['reason']
+ elif wordfilter['action'] == '3':
+ if not re.search(wordfilter['from'], message, re.DOTALL | re.IGNORECASE) is None:
+ raise UserError, '<meta http-equiv="refresh" content="%s;url=%s" />%s' % (wordfilter['redirect_time'], wordfilter['redirect_url'], wordfilter['reason'])
+ return message
+
+def checkNamefilters(name, tripcode, ip, board):
+ namefilters = FetchAll("SELECT * FROM `filters` WHERE `type` = '1'")
+
+ for namefilter in namefilters:
+ if namefilter["boards"] != "":
+ boards = pickle.loads(namefilter["boards"])
+ if namefilter["boards"] == "" or board in boards:
+ # check if this filter applies
+ match = False
+
+ if namefilter['from'] and namefilter['from_trip']:
+ # both name and trip filter
+ if re.search(namefilter['from'], name, re.DOTALL | re.IGNORECASE) and tripcode == namefilter['from_trip']:
+ match = True
+ elif namefilter['from'] and not namefilter['from_trip']:
+ # name filter
+ if re.search(namefilter['from'], name, re.DOTALL | re.IGNORECASE):
+ match = True
+ elif not namefilter['from'] and namefilter['from_trip']:
+ # trip filter
+ if tripcode == namefilter['from_trip']:
+ match = True
+
+ if match:
+ # do action
+ if namefilter['action'] == '0':
+ raise UserError, namefilter['reason']
+ elif namefilter['action'] == '1':
+ name = namefilter['to']
+ tripcode = ''
+ return name, tripcode
+ elif namefilter['action'] == '2':
+ # Ban
+ if namefilter['seconds'] != '0':
+ until = str(timestamp() + int(namefilter['seconds']))
+ else:
+ until = '0'
+
+ InsertDb("INSERT INTO `bans` (`ip`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (" + \
+ "'" + _mysql.escape_string(ip) + "', '" + _mysql.escape_string(namefilter['boards']) + \
+ "', " + str(timestamp()) + ", " + until + ", 'System', '" + _mysql.escape_string(namefilter['reason']) + \
+ "', 'Name Auto-ban', '"+_mysql.escape_string(namefilter['blind'])+"')")
+ regenerateAccess()
+ raise UserError, namefilter['reason']
+ elif namefilter['action'] == '3':
+ raise UserError, '<meta http-equiv="refresh" content="%s;url=%s" />%s' % (namefilter['redirect_time'], namefilter['redirect_url'], namefilter['reason'])
+ return name, tripcode
diff --git a/cgi/framework.py b/cgi/framework.py
new file mode 100644
index 0000000..4c89bb7
--- /dev/null
+++ b/cgi/framework.py
@@ -0,0 +1,467 @@
+# coding=utf-8
+import os
+import cgi
+import datetime
+import time
+import hashlib
+import pickle
+import socket
+import _mysql
+import urllib
+import re
+from Cookie import SimpleCookie
+
+from settings import Settings
+from database import *
+
+class CLT(datetime.tzinfo):
+ """
+ Clase para zona horaria chilena.
+ Como el gobierno nos tiene los horarios de verano para la pura cagá,
+ por mientras dejo el DST como un boolean. Cuando lo fijen, dejarlo automático.
+ """
+ def __init__(self):
+ self.isdst = False
+
+ def utcoffset(self, dt):
+ #return datetime.timedelta(hours=-3) + self.dst(dt)
+ return datetime.timedelta(hours=Settings.TIME_ZONE)
+
+ def dst(self, dt):
+ if self.isdst:
+ return datetime.timedelta(hours=1)
+ else:
+ return datetime.timedelta(0)
+
+ def tzname(self,dt):
+ return "GMT -3"
+
+def setBoard(dir):
+ """
+ Sets the board which the script is operating on by filling Settings._.BOARD
+ with the data from the db.
+ """
+ if not dir:
+ raise UserError, _("The specified board is invalid.")
+ logTime("Seteando el board " + dir)
+ board = FetchOne("SELECT * FROM `boards` WHERE `dir` = '%s' LIMIT 1" % _mysql.escape_string(dir))
+ if not board:
+ raise UserError, _("The specified board is invalid.")
+
+ board["filetypes"] = FetchAll("SELECT * FROM `boards_filetypes` INNER JOIN `filetypes` ON filetypes.id = boards_filetypes.filetypeid WHERE `boardid` = %s ORDER BY `ext` ASC" % _mysql.escape_string(board['id']))
+ board["filetypes_ext"] = [filetype['ext'] for filetype in board['filetypes']]
+ logTime("Board seteado.")
+
+ Settings._.BOARD = board
+
+ return board
+
+def addressIsBanned(ip, board):
+ packed_ip = inet_aton(ip)
+ bans = FetchAll("SELECT * FROM `bans` WHERE (`netmask` IS NULL AND `ip` = '"+str(packed_ip)+"') OR (`netmask` IS NOT NULL AND '"+str(packed_ip)+"' & `netmask` = `ip`)")
+ logTime("SELECT * FROM `bans` WHERE (`netmask` IS NULL AND `ip` = '"+str(packed_ip)+"') OR (`netmask` IS NOT NULL AND '"+str(packed_ip)+"' & `netmask` = `ip`)")
+ for ban in bans:
+ if ban["boards"] != "":
+ boards = pickle.loads(ban["boards"])
+ if ban["boards"] == "" or board in boards:
+ if board not in Settings.EXCLUDE_GLOBAL_BANS:
+ return True
+ return False
+
+def addressIsTor(ip):
+ if Settings._.IS_TOR is None:
+ res = False
+ nodes = []
+ if ip == '127.0.0.1': # Tor proxy address
+ res = True
+ else:
+ with open('tor.txt') as f:
+ nodes = [line.rstrip() for line in f]
+ if ip in nodes:
+ res = True
+ Settings._.IS_TOR = res
+ return res
+ else:
+ return Settings._.IS_TOR
+
+def addressIsProxy(ip):
+ if Settings._.IS_PROXY is None:
+ res = False
+ proxies = []
+ with open('proxy.txt') as f:
+ proxies = [line.rstrip() for line in f]
+ if ip in proxies:
+ res = True
+ Settings._.IS_PROXY = res
+ return res
+ else:
+ return Settings._.IS_PROXY
+
+def addressIsES(ip):
+ ES = ['AR', 'BO', 'CL', 'CO', 'CR', 'CU', 'EC', 'ES', 'GF',
+ 'GY', 'GT', 'HN', 'MX', 'NI', 'PA', 'PE', 'PY', 'PR', 'SR', 'UY', 'VE'] # 'BR',
+ return getCountry(ip) in ES
+
+def getCountry(ip):
+ import geoip
+ return geoip.country(ip)
+
+def getHost(ip):
+ if Settings._.HOST is None:
+ try:
+ Settings._.HOST = socket.gethostbyaddr(ip)[0]
+ return Settings._.HOST
+ except socket.herror:
+ return None
+ else:
+ return Settings._.HOST
+
+def hostIsBanned(ip):
+ host = getHost(ip)
+ if host:
+ banned_hosts = []
+ for banned_host in banned_hosts:
+ if host.endswith(banned_host):
+ return True
+ return False
+ else:
+ return False
+
+def updateBoardSettings():
+ """
+ Pickle the board's settings and store it in the configuration field
+ """
+ board = Settings._.BOARD
+ #UpdateDb("UPDATE `boards` SET `configuration` = '%s' WHERE `id` = %s LIMIT 1" % (_mysql.escape_string(configuration), board["id"]))
+
+ del board["filetypes"]
+ del board["filetypes_ext"]
+ post_values = ["`" + _mysql.escape_string(str(key)) + "` = '" + _mysql.escape_string(str(value)) + "'" for key, value in board.iteritems()]
+
+ UpdateDb("UPDATE `boards` SET %s WHERE `id` = '%s' LIMIT 1" % (", ".join(post_values), board["id"]))
+
+def timestamp(t=None):
+ """
+ Create MySQL-safe timestamp from the datetime t if provided, otherwise create
+ the timestamp from datetime.now()
+ """
+ if not t:
+ t = datetime.datetime.now()
+ return int(time.mktime(t.timetuple()))
+
+def formatDate(t=None, home=False):
+ """
+ Format a datetime to a readable date
+ """
+ if not t:
+ t = datetime.datetime.now(CLT())
+ # Timezone fix
+ #t += datetime.timedelta(hours=1)
+
+ days = {'en': ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
+ 'es': ['lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'],
+ 'jp': ['月', 'ç«', 'æ°´', '木', '金', '土', 'æ—¥']}
+
+ daylist = days[Settings.LANG]
+ format = "%d/%m/%y(%a)%H:%M:%S"
+
+ if not home:
+ try:
+ board = Settings._.BOARD
+ if board["dir"] == 'world':
+ daylist = days['en']
+ elif board["dir"] == '2d':
+ daylist = days['jp']
+ except:
+ pass
+
+ t = t.strftime(format)
+
+ t = re.compile(r"mon", re.DOTALL | re.IGNORECASE).sub(daylist[0], t)
+ t = re.compile(r"tue", re.DOTALL | re.IGNORECASE).sub(daylist[1], t)
+ t = re.compile(r"wed", re.DOTALL | re.IGNORECASE).sub(daylist[2], t)
+ t = re.compile(r"thu", re.DOTALL | re.IGNORECASE).sub(daylist[3], t)
+ t = re.compile(r"fri", re.DOTALL | re.IGNORECASE).sub(daylist[4], t)
+ t = re.compile(r"sat", re.DOTALL | re.IGNORECASE).sub(daylist[5], t)
+ t = re.compile(r"sun", re.DOTALL | re.IGNORECASE).sub(daylist[6], t)
+ return t
+
+def formatTimestamp(t, home=False):
+ """
+ Format a timestamp to a readable date
+ """
+ return formatDate(datetime.datetime.fromtimestamp(int(t), CLT()), home)
+
+def timeTaken(time_start, time_finish):
+ return str(round(time_finish - time_start, 3))
+
+def parseIsoPeriod(t_str):
+ m = re.match('P(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(\d+)S', t_str)
+ if m:
+ grps = [x for x in m.groups() if x]
+ if len(grps) == 1:
+ grps.insert(0, '0')
+ grps[-1] = grps[-1].zfill(2)
+ return ':'.join(grps)
+ else:
+ return '???'
+
+def getFormData(self):
+ """
+ Process input sent to WSGI through a POST method and output it in an easy to
+ retrieve format: dictionary of dictionaries in the format of {key: value}
+ """
+ wsgi_input = self.environ["wsgi.input"]
+ post_form = self.environ.get("wsgi.post_form")
+ if (post_form is not None
+ and post_form[0] is wsgi_input):
+ return post_form[2]
+ # This must be done to avoid a bug in cgi.FieldStorage
+ self.environ.setdefault("QUERY_STRING", "")
+ fs = cgi.FieldStorage(fp=wsgi_input,
+ environ=self.environ,
+ keep_blank_values=1)
+ new_input = InputProcessed()
+ post_form = (new_input, wsgi_input, fs)
+ self.environ["wsgi.post_form"] = post_form
+ self.environ["wsgi.input"] = new_input
+
+ formdata = {}
+ for key in dict(fs):
+ try:
+ formdata.update({key: fs[key].value})
+ if key == "file":
+ formdata.update({"file_original": secure_filename(fs[key].filename)})
+ except AttributeError:
+ formdata.update({key: fs[key]})
+
+ return formdata
+
+class InputProcessed(object):
+ def read(self):
+ raise EOFError("El stream de wsgi.input ya se ha consumido.")
+ readline = readlines = __iter__ = read
+
+class UserError(Exception):
+ pass
+
+def secure_filename(path):
+ split = re.compile(r'[\0%s]' % re.escape(''.join([os.path.sep, os.path.altsep or ''])))
+ return cgi.escape(split.sub('', path))
+
+def getMD5(data):
+ m = hashlib.md5()
+ m.update(data)
+
+ return m.hexdigest()
+
+def nullstr(len): return "\0" * len
+
+def hide_data(data, length, key, secret):
+ """
+ Encrypts data, useful for tripcodes and IDs
+ """
+ crypt = rc4(nullstr(length), rc4(nullstr(32), key + secret) + data).encode('base64')
+ return crypt.rstrip('\n')
+
+def rc4(data, key):
+ """
+ rc4 implementation
+ """
+ x = 0
+ box = range(256)
+ for i in range(256):
+ x = (x + box[i] + ord(key[i % len(key)])) % 256
+ box[i], box[x] = box[x], box[i]
+ x = 0
+ y = 0
+ out = []
+ for char in data:
+ x = (x + 1) % 256
+ y = (y + box[x]) % 256
+ box[x], box[y] = box[y], box[x]
+ out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256]))
+
+ return ''.join(out)
+
+def getRandomLine(filename):
+ import random
+ f = open(filename, 'r')
+ lines = f.readlines()
+ num = random.randint(0, len(lines) - 1)
+ return lines[num]
+
+def getRandomIco():
+ from glob import glob
+ from random import choice
+ icons = glob("../static/ico/*")
+ if icons:
+ return choice(icons).lstrip('..')
+ else:
+ return ''
+
+def N_(message): return message
+
+def getCookie(self, value=""):
+ return urllib.unquote_plus(self._cookies[value].value)
+
+def reCookie(self, key, value=""):
+ board = Settings._.BOARD
+ setCookie(self, key, value)
+
+def setCookie(self, key, value="", max_age=None, expires=None, path="/", domain=None, secure=None):
+ """
+ Copied from Colubrid
+ """
+ if self._cookies is None:
+ self._cookies = SimpleCookie()
+ self._cookies[key] = urllib.quote_plus(value)
+ if not max_age is None:
+ self._cookies[key]["max-age"] = max_age
+ if not expires is None:
+ if isinstance(expires, basestring):
+ self._cookies[key]["expires"] = expires
+ expires = None
+ elif isinstance(expires, datetime):
+ expires = expires.utctimetuple()
+ elif not isinstance(expires, (int, long)):
+ expires = datetime.datetime.gmtime(expires)
+ else:
+ raise ValueError("Se requiere de un entero o un datetime")
+ if not expires is None:
+ now = datetime.datetime.gmtime()
+ month = _([N_("Jan"), N_("Feb"), N_("Mar"), N_("Apr"), N_("May"), N_("Jun"), N_("Jul"),
+ N_("Aug"), N_("Sep"), N_("Oct"), N_("Nov"), N_("Dec")][now.tm_mon - 1])
+ day = _([N_("Monday"), N_("Tuesday"), N_("Wednesday"), N_("Thursday"),
+ N_("Friday"), N_("Saturday"), N_("Sunday")][expires.tm_wday])
+ date = "%02d-%s-%s" % (
+ now.tm_mday, month, str(now.tm_year)[-2:]
+ )
+ d = "%s, %s %02d:%02d:%02d GMT" % (day, date, now.tm_hour,
+ now.tm_min, now.tm_sec)
+ self._cookies[key]["expires"] = d
+ if not path is None:
+ self._cookies[key]["path"] = path
+ if not domain is None:
+ if domain != "THIS":
+ self._cookies[key]["domain"] = domain
+ else:
+ self._cookies[key]["domain"] = Settings.DOMAIN
+ if not secure is None:
+ self._cookies[key]["secure"] = secure
+
+def deleteCookie(self, key):
+ """
+ Copied from Colubrid
+ """
+ if self._cookies is None:
+ self._cookies = SimpleCookie()
+ if not key in self._cookies:
+ self._cookies[key] = ""
+ self._cookies[key]["max-age"] = 0
+
+def elapsed_time(seconds, suffixes=['y','w','d','h','m','s'], add_s=False, separator=' '):
+ """
+ Takes an amount of seconds and turns it into a human-readable amount of time.
+ """
+ # the formatted time string to be returned
+ time = []
+
+ # the pieces of time to iterate over (days, hours, minutes, etc)
+ # - the first piece in each tuple is the suffix (d, h, w)
+ # - the second piece is the length in seconds (a day is 60s * 60m * 24h)
+ parts = [(suffixes[0], 60 * 60 * 24 * 7 * 52),
+ (suffixes[1], 60 * 60 * 24 * 7),
+ (suffixes[2], 60 * 60 * 24),
+ (suffixes[3], 60 * 60),
+ (suffixes[4], 60),
+ (suffixes[5], 1)]
+
+ # for each time piece, grab the value and remaining seconds, and add it to
+ # the time string
+ for suffix, length in parts:
+ value = seconds / length
+ if value > 0:
+ seconds = seconds % length
+ time.append('%s%s' % (str(value),
+ (suffix, (suffix, suffix + 's')[value > 1])[add_s]))
+ if seconds < 1:
+ break
+
+ return separator.join(time)
+
+def inet_aton(ip_string):
+ import socket, struct
+ return struct.unpack('!L',socket.inet_aton(ip_string))[0]
+
+def inet_ntoa(packed_ip):
+ import socket, struct
+ return socket.inet_ntoa(struct.pack('!L',packed_ip))
+
+def is_bad_proxy(pip):
+ import urllib2
+ import socket
+ socket.setdefaulttimeout(3)
+
+ try:
+ proxy_handler = urllib2.ProxyHandler({'http': pip})
+ opener = urllib2.build_opener(proxy_handler)
+ opener.addheaders = [('User-agent', 'Mozilla/5.0')]
+ urllib2.install_opener(opener)
+ req=urllib2.Request('http://bienvenidoainternet.org')
+ sock=urllib2.urlopen(req)
+ except urllib2.HTTPError, e:
+ return e.code
+ except Exception, detail:
+ return True
+ return False
+
+def send_mail(subject, srcmsg):
+ import smtplib
+ from email.mime.text import MIMEText
+
+ msg = MIMEText(srcmsg)
+ me = 'weabot@bienvenidoainternet.org'
+ you = 'burocracia@bienvenidoainternet.org'
+
+ msg['Subject'] = 'The contents of %s' % textfile
+ msg['From'] = me
+ msg['To'] = you
+
+ s = smtplib.SMTP('localhost')
+ s.sendmail(me, [you], msg.as_string())
+ s.quit()
+
+class weabotLogger:
+ def __init__(self):
+ self.times = []
+
+ def log(self, message):
+ self.times.append([time.time(), message])
+
+ def allTimes(self):
+ output = "Time Logged action\n--------------------------\n"
+ start = self.times[0][0]
+ for time in self.times:
+ difference = str(time[0] - start)
+ difference_split = difference.split(".")
+ if len(difference_split[0]) < 2:
+ difference_split[0] = "0" + difference_split[0]
+
+ if len(difference_split[1]) < 7:
+ difference_split[1] = ("0" * (7 - len(difference_split[1]))) + difference_split[1]
+ elif len(difference_split[1]) > 7:
+ difference_split[1] = difference_split[1][:7]
+
+ output += ".".join(difference_split) + " " + time[1] + "\n"
+
+ return output
+
+logger = weabotLogger()
+def logTime(message):
+ global logger
+ logger.log(message)
+
+def logTimes():
+ global logger
+ return logger.allTimes()
diff --git a/cgi/geoip.py b/cgi/geoip.py
new file mode 100644
index 0000000..0bcb3d8
--- /dev/null
+++ b/cgi/geoip.py
@@ -0,0 +1,128 @@
+"""Python API that wraps GeoIP country database lookup into a simple function.
+
+Download the latest MaxMind GeoIP country database and read other docs here:
+ http://www.maxmind.com/app/geolitecountry
+
+Copyright (C) 2009 Ben Hoyt, released under the Lesser General Public License:
+ http://www.gnu.org/licenses/lgpl.txt
+
+Usage examples:
+
+>>> country('64.233.161.99')
+'US'
+>>> country('202.21.128.102')
+'NZ'
+>>> country('asdf')
+''
+>>> country('127.0.0.1')
+''
+"""
+
+# List of country codes (indexed by GeoIP country ID number)
+countries = (
+ '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ',
+ 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH',
+ 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA',
+ 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU',
+ 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG',
+ 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'FX', 'GA', 'GB',
+ 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT',
+ 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN',
+ 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM',
+ 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS',
+ 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN',
+ 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA',
+ 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA',
+ 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY',
+ 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI',
+ 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD',
+ 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW',
+ 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN',
+ 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1',
+ 'AX', 'GG', 'IM', 'JE', 'BL', 'MF')
+
+def iptonum(ip):
+ """Convert IP address string to 32-bit integer, or return None if IP is bad.
+
+ >>> iptonum('0.0.0.0')
+ 0
+ >>> hex(iptonum('127.0.0.1'))
+ '0x7f000001'
+ >>> hex(iptonum('255.255.255.255'))
+ '0xffffffffL'
+ >>> iptonum('127.0.0.256')
+ >>> iptonum('1.2.3')
+ >>> iptonum('a.s.d.f')
+ >>> iptonum('1.2.3.-4')
+ >>> iptonum('')
+ """
+ segments = ip.split('.')
+ if len(segments) != 4:
+ return None
+ num = 0
+ for segment in segments:
+ try:
+ segment = int(segment)
+ except ValueError:
+ return None
+ if segment < 0 or segment > 255:
+ return None
+ num = num << 8 | segment
+ return num
+
+class DatabaseError(Exception):
+ pass
+
+class GeoIP(object):
+ """Wraps GeoIP country database lookup into a class."""
+
+ _record_length = 3
+ _country_start = 16776960
+
+ def __init__(self, dbname='GeoIP.dat'):
+ """Init GeoIP instance with given GeoIP country database file."""
+ self._dbfile = open(dbname, 'rb')
+
+ def country(self, ip):
+ """Lookup IP address string and turn it into a two-letter country code
+ like 'NZ', or return empty string if unknown.
+
+ >>> g = GeoIP()
+ >>> g.country('64.233.161.99')
+ 'US'
+ >>> g.country('202.21.128.102')
+ 'NZ'
+ >>> g.country('asdf')
+ ''
+ >>> g.country('127.0.0.1')
+ ''
+ """
+ ipnum = iptonum(ip)
+ if ipnum is None:
+ return ''
+ return countries[self._country_id(ipnum)]
+
+ def _country_id(self, ipnum):
+ """Look up and return country ID of given 32-bit IP address."""
+ # Search algorithm from: http://code.google.com/p/pygeoip/
+ offset = 0
+ for depth in range(31, -1, -1):
+ self._dbfile.seek(offset * 2 * self._record_length)
+ data = self._dbfile.read(2 * self._record_length)
+ x = [0, 0]
+ for i in range(2):
+ for j in range(self._record_length):
+ x[i] += ord(data[self._record_length * i + j]) << (j * 8)
+ i = 1 if ipnum & (1 << depth) else 0
+ if x[i] >= self._country_start:
+ return x[i] - self._country_start
+ offset = x[i]
+ raise DatabaseError('GeoIP database corrupt: offset=%s' % offset)
+
+def country(ip, dbname='GeoIP.dat'):
+ """Helper function that creates a GeoIP instance and calls country()."""
+ return GeoIP(dbname).country(ip)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/cgi/img.py b/cgi/img.py
new file mode 100644
index 0000000..21f326a
--- /dev/null
+++ b/cgi/img.py
@@ -0,0 +1,416 @@
+# coding=utf-8
+import struct
+import math
+#import random
+import os
+import subprocess
+from StringIO import StringIO
+
+from settings import Settings
+from database import *
+from framework import *
+
+try: # Windows needs stdio set for binary mode.
+ import msvcrt
+ msvcrt.setmode (0, os.O_BINARY) # stdin = 0
+ msvcrt.setmode (1, os.O_BINARY) # stdout = 1
+except ImportError:
+ pass
+
+def processImage(post, data, t, originalname, spoiler=False):
+ """
+ Take all post data from <post>, process uploaded file in <data>, and calculate
+ file names using datetime <t>
+ Returns updated <post> with file and thumb values
+ """
+ board = Settings._.BOARD
+
+ used_filetype = None
+
+ # get image information
+ content_type, width, height, size, extra = getImageInfo(data)
+
+ # check the size is fine
+ if size > int(board["maxsize"])*1024:
+ raise UserError, _("File too big. The maximum file size is: %s") % board['maxsize']
+
+ # check if file is supported
+ for filetype in board['filetypes']:
+ if content_type == filetype['mime']:
+ used_filetype = filetype
+ break
+
+ if not used_filetype:
+ raise UserError, _("File type not supported.")
+
+ # check if file is already posted
+ is_duplicate = checkFileDuplicate(data)
+ if checkFileDuplicate(data)[0]:
+ raise UserError, _("This image has already been posted %s.") % ('<a href="' + Settings.BOARDS_URL + board['dir'] + '/res/' + str(is_duplicate[1]) + '.html#' + str(is_duplicate[2]) + '">' + _("here") + '</a>')
+
+ # prepare file names
+ if used_filetype['preserve_name'] == '1':
+ file_base = os.path.splitext(originalname)[0] # use original filename
+ else:
+ file_base = '%d' % int(t * 1000) # generate timestamp name
+ file_name = file_base + "." + used_filetype['ext']
+ file_thumb_name = file_base + "s.jpg"
+
+ # prepare paths
+ file_path = Settings.IMAGES_DIR + board["dir"] + "/src/" + file_name
+ file_thumb_path = Settings.IMAGES_DIR + board["dir"] + "/thumb/" + file_thumb_name
+ file_mobile_path = Settings.IMAGES_DIR + board["dir"] + "/mobile/" + file_thumb_name
+ file_cat_path = Settings.IMAGES_DIR + board["dir"] + "/cat/" + file_thumb_name
+
+ # remove EXIF data if necessary for privacy
+ if content_type == 'image/jpeg':
+ data = removeExifData(data)
+
+ # write file
+ f = open(file_path, "wb")
+ try:
+ f.write(data)
+ finally:
+ f.close()
+
+ # set maximum dimensions
+ maxsize = int(board['thumb_px'])
+
+ post["file"] = file_name
+ post["image_width"] = width
+ post["image_height"] = height
+
+ # Do we need to thumbnail it?
+ if not used_filetype['image']:
+ # make thumbnail
+ file_thumb_width, file_thumb_height = getThumbDimensions(width, height, maxsize)
+
+ if used_filetype['ffmpeg_thumb'] == '1':
+ # use ffmpeg to make thumbnail
+ logTime("Generating thumbnail")
+
+ if used_filetype['mime'][:5] == 'video':
+ #duration_half = str(int(extra['duration'] / 2))
+ retcode = subprocess.call([
+ Settings.FFMPEG_PATH, '-strict', '-2', '-ss', '0', '-i', file_path,
+ '-v', 'quiet', '-an', '-vframes', '1', '-f', 'mjpeg', '-vf', 'scale=%d:%d' % (file_thumb_width, file_thumb_height),
+ '-threads', '1', file_thumb_path])
+ if spoiler:
+ args = [Settings.CONVERT_PATH, file_thumb_path, "-limit", "thread", "1", "-background", "white", "-flatten", "-resize", "%dx%d" % (file_thumb_width, file_thumb_height), "-blur", "0x12", "-gravity", "center", "-fill", "rgba(0,0,0, .6)", "-draw", "rectangle 0,%d,%d,%d" % ((file_thumb_height/2)-10, file_thumb_width, (file_thumb_height/2)+7), "-fill", "white", "-annotate", "0", "Alerta de spoiler", "-quality", str(Settings.THUMB_QUALITY), file_thumb_path]
+ retcode = subprocess.call(args)
+ elif used_filetype['mime'][:5] == 'audio':
+ # we do an exception and use png for audio waveform thumbnails since they
+ # 1. are smaller 2. allow for transparency
+ file_thumb_name = file_thumb_name[:-3] + "png"
+ file_thumb_path = file_thumb_path[:-3] + "png"
+ file_mobile_path = file_mobile_path[:-3] + "png"
+ file_cat_path = file_cat_path[:-3] + "png"
+
+ if int(board['thumb_px']) > 149:
+ file_thumb_width = board['thumb_px']
+ file_thumb_height = float(int(board['thumb_px'])/2)
+ else:
+ file_thumb_width = 150
+ file_thumb_height = 75
+
+ retcode = subprocess.call([
+ Settings.FFMPEG_PATH, '-t', '300', '-i', file_path,
+ '-filter_complex', 'showwavespic=s=%dx%d:split_channels=1' % (int(file_thumb_width), int(file_thumb_height)),
+ '-frames:v', '1', '-threads', '1', file_thumb_path])
+# elif used_filetype['mime'] == 'application/x-shockwave-flash' or used_filetype['mime'] == 'mime/x-shockwave-flash':
+# retcode = subprocess.call([
+# './ffmpeg', '-i', file_path, '-vcodec', 'mjpeg', '-vframes', '1', '-an', '-f', 'rawvideo',
+# '-vf', 'scale=%d:%d' % (file_thumb_width, file_thumb_height), '-threads', '1', file_thumb_path])
+
+ if retcode != 0:
+ os.remove(file_path)
+ raise UserError, _("Thumbnail creation failure.") + ' ('+str(retcode)+')'
+ else:
+ # use imagemagick to make thumbnail
+ args = [Settings.CONVERT_PATH, file_path, "-limit", "thread", "1", "-background", "white", "-flatten", "-resize", "%dx%d" % (file_thumb_width, file_thumb_height)]
+ if spoiler:
+ args += ["-blur", "0x12", "-gravity", "center", "-fill", "rgba(0,0,0, .6)", "-draw", "rectangle 0,%d,%d,%d" % ((file_thumb_height/2)-10, file_thumb_width, (file_thumb_height/2)+7), "-fill", "white", "-annotate", "0", "Alerta de spoiler"]
+ args += ["-quality", str(Settings.THUMB_QUALITY), file_thumb_path]
+
+ # generate thumbnails
+ logTime("Generating thumbnail")
+ retcode = subprocess.call(args)
+ if retcode != 0:
+ os.remove(file_path)
+ raise UserError, _("Thumbnail creation failure.") + ' ('+str(retcode)+')'
+
+ # check if thumbnail was truly created
+ try:
+ open(file_thumb_path)
+ except:
+ os.remove(file_path)
+ raise UserError, _("Thumbnail creation failure.")
+
+ # create extra thumbnails (catalog/mobile)
+ subprocess.call([Settings.CONVERT_PATH, file_thumb_path, "-limit" , "thread", "1", "-resize", "100x100", "-quality", "75", file_mobile_path])
+ if not post["parentid"]:
+ subprocess.call([Settings.CONVERT_PATH, file_thumb_path, "-limit" , "thread", "1", "-resize", "150x150", "-quality", "60", file_cat_path])
+
+ post["thumb"] = file_thumb_name
+ post["thumb_width"] = file_thumb_width
+ post["thumb_height"] = file_thumb_height
+ else:
+ # Don't thumbnail and use mime image
+ if board["board_type"] == '0':
+ post["thumb"] = used_filetype['image']
+ post["thumb_width"] = '120'
+ post["thumb_height"] = '120'
+ else:
+ post["thumb"] = used_filetype['image'].split(".")[0] + '_small.png'
+ post["thumb_width"] = '90'
+ post["thumb_height"] = '90'
+
+ # calculate size (bytes)
+ post["file_size"] = len(data)
+
+ # add additional metadata, if any
+ post["message"] += extraInfo(content_type, file_name, file_path)
+
+ # file md5
+ post["file_hex"] = getMD5(data)
+
+ return post
+
+def extraInfo(mime, file_name, file_path):
+ board = Settings._.BOARD
+
+ if mime in ['audio/ogg', 'audio/opus', 'audio/mpeg', 'video/webm']:
+ info = ffprobe_f(file_path)
+ extra = {}
+ credit_str = ""
+
+ if mime == 'video/webm':
+ for s in info['streams']:
+ if 'width' in s:
+ stream = s
+ else:
+ stream = info['streams'][0]
+
+ extra['codec'] = stream.get('codec_name', '').encode('utf-8')
+ format = info['format']
+
+ if 'bit_rate' in format:
+ extra['codec'] += ' ~%d kbps' % int(int(format['bit_rate']) / 1000)
+ if 'tags' in format:
+ extra['title'] = format['tags'].get('TITLE', format['tags'].get('title', '')).encode('utf-8')
+ extra['artist'] = format['tags'].get('ARTIST', format['tags'].get('artist', '')).encode('utf-8')
+ if extra['title'] or extra['artist']:
+ credit_str = ' - '.join((extra['artist'], extra['title'])) + ' '
+ if 'tags' in stream:
+ extra['title'] = stream['tags'].get('TITLE', '').encode('utf-8')
+ extra['artist'] = stream['tags'].get('ARTIST', '').encode('utf-8')
+ if extra['title'] or extra['artist']:
+ credit_str = ' - '.join((extra['artist'], extra['title'])) + ' '
+
+ return '<hr /><small>%s(%s)</small>' % (credit_str, extra['codec'])
+
+ elif mime in ['audio/mod', 'audio/xm', 'audio/s3m']:
+ ext = mime.split('/')[1].upper()
+ url = '/cgi/play/%s/%s' % (board['dir'], file_name)
+ return '<hr /><small>Módulo tracker (%s) [<a href="%s" target="_blank">Click para escuchar</a>]</small>' % (ext, url)
+
+ return ''
+
+def getImageInfo(data):
+ data = str(data)
+ size = len(data)
+ height = -1
+ width = -1
+ extra = {}
+ content_type = ""
+
+ # handle GIFs
+ if (size >= 10) and data[:6] in ("GIF87a", "GIF89a"):
+ # Check to see if content_type is correct
+ content_type = "image/gif"
+ w, h = struct.unpack("<HH", data[6:10])
+ width = int(w)
+ height = int(h)
+
+ # See PNG 2. Edition spec (http://www.w3.org/TR/PNG/)
+ # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
+ # and finally the 4-byte width, height
+ elif ((size >= 24) and data.startswith("\211PNG\r\n\032\n")
+ and (data[12:16] == "IHDR")):
+ content_type = "image/png"
+ w, h = struct.unpack(">LL", data[16:24])
+ width = int(w)
+ height = int(h)
+
+ # Maybe this is for an older PNG version.
+ elif (size >= 16) and data.startswith("\211PNG\r\n\032\n"):
+ # Check to see if we have the right content type
+ content_type = "image/png"
+ w, h = struct.unpack(">LL", data[8:16])
+ width = int(w)
+ height = int(h)
+
+ # handle JPEGs
+ elif (size >= 2) and data.startswith("\377\330"):
+ content_type = "image/jpeg"
+ jpeg = StringIO(data)
+ jpeg.read(2)
+ b = jpeg.read(1)
+ try:
+ while (b and ord(b) != 0xDA):
+ while (ord(b) != 0xFF): b = jpeg.read
+ while (ord(b) == 0xFF): b = jpeg.read(1)
+ if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
+ jpeg.read(3)
+ h, w = struct.unpack(">HH", jpeg.read(4))
+ break
+ else:
+ jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
+ b = jpeg.read(1)
+ width = int(w)
+ height = int(h)
+ except struct.error:
+ pass
+ except ValueError:
+ pass
+
+ # handle WebM
+ elif (size >= 4) and data.startswith("\x1A\x45\xDF\xA3"):
+ content_type = "video/webm"
+ info = ffprobe(data)
+
+ for stream in info['streams']:
+ if 'width' in stream:
+ width = stream['width']
+ height = stream['height']
+ break
+
+ extra['duration'] = float(info['format']['duration'])
+
+ # handle ogg formats (vorbis/opus)
+ elif (size >= 64) and data[:4] == "OggS":
+ if data[28:35] == "\x01vorbis":
+ content_type = "audio/ogg"
+ elif data[28:36] == "OpusHead":
+ content_type = "audio/opus"
+
+ # handle MP3
+ elif (size >= 64) and (data[:3] == "ID3" or data[:3] == "\xFF\xFB"):
+ content_type = "audio/mpeg"
+
+ # handle MOD
+ elif (size >= 64) and data[1080:1084] == "M.K.":
+ content_type = "audio/mod"
+
+ # handle XM
+ elif (size >= 64) and data.startswith("Extended Module:"):
+ content_type = "audio/xm"
+
+ # handle S3M
+ elif (size >= 64) and data[25:32] == "\x00\x00\x00\x1A\x10\x00\x00":
+ content_type = "audio/s3m"
+
+ # handle PDF
+ elif (size >= 4) and data[:7] == "%PDF-1.":
+ content_type = "application/pdf"
+
+ # handle Shockwave Flash
+ elif (size >= 3) and data[:3] in ["CWS", "FWS"]:
+ content_type = "application/x-shockwave-flash"
+
+ # handle torrent
+ elif (size >= 11) and data[:11] == "d8:announce":
+ content_type = "application/x-bittorrent"
+
+ # handle PDF
+ elif (size >= 2) and data[:2] == "PK":
+ content_type = "application/epub+zip"
+
+ return content_type, width, height, size, extra
+
+def ffprobe(data):
+ import json
+ p = subprocess.Popen([Settings.FFPROBE_PATH, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', '-'],
+ stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ out = p.communicate(input=data)[0]
+ return json.loads(out)
+
+def ffprobe_f(filename):
+ import json
+
+ p = subprocess.Popen([Settings.FFPROBE_PATH, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filename],
+ stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ out = p.communicate()[0]
+ return json.loads(out)
+
+def getThumbDimensions(width, height, maxsize):
+ """
+ Calculate dimensions to use for a thumbnail with maximum width/height of
+ <maxsize>, keeping aspect ratio
+ """
+ wratio = (float(maxsize) / float(width))
+ hratio = (float(maxsize) / float(height))
+
+ if (width <= maxsize) and (height <= maxsize):
+ return width, height
+ else:
+ if (wratio * height) < maxsize:
+ thumb_height = math.ceil(wratio * height)
+ thumb_width = maxsize
+ else:
+ thumb_width = math.ceil(hratio * width)
+ thumb_height = maxsize
+
+ return int(thumb_width), int(thumb_height)
+
+def checkFileDuplicate(data):
+ """
+ Check that the file <data> does not already exist in a live post on the
+ current board by calculating its hex and checking it against the database
+ """
+ board = Settings._.BOARD
+
+ file_hex = getMD5(data)
+ post = FetchOne("SELECT `id`, `parentid` FROM `posts` WHERE `file_hex` = '%s' AND `boardid` = %s AND IS_DELETED = 0 LIMIT 1" % (file_hex, board['id']))
+ if post:
+ if int(post["parentid"]) != 0:
+ return True, post["parentid"], post["id"]
+ else:
+ return True, post["id"], post["id"]
+ else:
+ return False, 0, 0
+
+def getJpegSegments(data):
+ if data[0:2] != b"\xff\xd8":
+ raise UserError("Given data isn't JPEG.")
+
+ head = 2
+ segments = [b"\xff\xd8"]
+ while 1:
+ if data[head: head + 2] == b"\xff\xda":
+ yield data[head:]
+ break
+ else:
+ length = struct.unpack(">H", data[head + 2: head + 4])[0]
+ endPoint = head + length + 2
+ seg = data[head: endPoint]
+ yield seg
+ head = endPoint
+
+ if (head >= len(data)):
+ raise UserDataError("Wrong JPEG data.")
+
+def removeExifData(src_data):
+ exif = None
+
+ for seg in getJpegSegments(src_data):
+ if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
+ exif = seg
+ break
+
+ if exif:
+ return src_data.replace(exif, b"")
+ else:
+ return src_data
diff --git a/cgi/locale/es/LC_MESSAGES/weabot.mo b/cgi/locale/es/LC_MESSAGES/weabot.mo
new file mode 100644
index 0000000..8e207a5
--- /dev/null
+++ b/cgi/locale/es/LC_MESSAGES/weabot.mo
Binary files differ
diff --git a/cgi/manage.py b/cgi/manage.py
new file mode 100644
index 0000000..44731ba
--- /dev/null
+++ b/cgi/manage.py
@@ -0,0 +1,1823 @@
+# coding=utf-8
+import _mysql
+import os
+import cgi
+import shutil
+import imaplib
+import poplib
+import datetime
+
+from database import *
+from settings import Settings
+from framework import *
+from formatting import *
+from template import *
+from post import *
+
+def manage(self, path_split):
+ page = ''
+ validated = False
+ administrator = False
+ moderator = True
+ skiptemplate = False
+
+ try:
+ if self.formdata['username'] and self.formdata['password']:
+ # If no admin accounts available, create admin:admin
+ first_admin = FetchOne("SELECT 1 FROM `staff` WHERE `rights` = 0 LIMIT 1", 0)
+ if not first_admin:
+ InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('admin', '" + _mysql.escape_string(genPasswd("admin")) + "', 0, 0)")
+
+ password = genPasswd(self.formdata['password'])
+
+ valid_account = FetchOne("SELECT * FROM `staff` WHERE `username` = '" + _mysql.escape_string(self.formdata['username']) + "' AND `password` = '" + _mysql.escape_string(password) + "' LIMIT 1")
+ if valid_account:
+ setCookie(self, 'weabot_manage', self.formdata['username'] + ':' + valid_account['password'], domain='THIS')
+ UpdateDb('DELETE FROM `logs` WHERE `timestamp` < ' + str(timestamp() - 604800)) # one week
+ else:
+ page += _('Incorrect username/password.')
+ logAction('', 'Failed log-in. U:'+_mysql.escape_string(self.formdata['username'])+' IP:'+self.environ["REMOTE_ADDR"])
+ except:
+ pass
+
+ try:
+ manage_cookie = getCookie(self, 'weabot_manage')
+ if manage_cookie != '':
+ username, password = manage_cookie.split(':')
+ staff_account = FetchOne("SELECT * FROM `staff` WHERE `username` = '" + _mysql.escape_string(username) + "' AND `password` = '" + _mysql.escape_string(password) + "' LIMIT 1")
+ if staff_account:
+ validated = True
+ if staff_account['rights'] == '0' or staff_account['rights'] == '1' or staff_account['rights'] == '2':
+ administrator = True
+ if staff_account['rights'] == '2':
+ moderator = False
+ UpdateDb('UPDATE `staff` SET `lastactive` = ' + str(timestamp()) + ' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1')
+ except:
+ pass
+
+ #validated = True
+ #moderator = True
+ #staff_account = {}
+ #staff_account['username'] = ''
+ #staff_account['rights'] = '0'
+ #staff_account['added'] = '0'
+
+ if not validated:
+ template_filename = "login.html"
+ template_values = {}
+ else:
+ if len(path_split) > 2:
+ if path_split[2] == 'rebuild':
+ if not administrator:
+ return
+
+ try:
+ board_dir = path_split[3]
+ except:
+ board_dir = ''
+
+ if board_dir == '':
+ template_filename = "rebuild.html"
+ template_values = {'boards': boardlist()}
+ else:
+ everything = ("everything" in self.formdata)
+ if board_dir == '!ALL':
+ t1 = time.time()
+ boards = FetchAll('SELECT `dir` FROM `boards` WHERE secret = 0')
+ for board in boards:
+ board = setBoard(board['dir'])
+ regenerateBoard(everything)
+
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('all boards'))
+ elif board_dir == '!BBS':
+ t1 = time.time()
+ boards = FetchAll('SELECT `dir` FROM `boards` WHERE `board_type` = 1')
+ for board in boards:
+ board = setBoard(board['dir'])
+ regenerateBoard(everything)
+
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('all boards'))
+ elif board_dir == '!IB':
+ t1 = time.time()
+ boards = FetchAll('SELECT `dir` FROM `boards` WHERE `board_type` = 1')
+ for board in boards:
+ board = setBoard(board['dir'])
+ regenerateBoard(everything)
+
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('all boards'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('all boards'))
+ elif board_dir == '!HOME':
+ t1 = time.time()
+ regenerateHome()
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('home'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('home'))
+ elif board_dir == '!NEWS':
+ t1 = time.time()
+ regenerateNews()
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('news'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('news'))
+ elif board_dir == '!KAKO':
+ t1 = time.time()
+ boards = FetchAll('SELECT `dir` FROM `boards` WHERE archive = 1')
+ for board in boards:
+ board = setBoard(board['dir'])
+ regenerateKako()
+
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': 'kako', 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % 'kako')
+ elif board_dir == '!HTACCESS':
+ t1 = time.time()
+ if regenerateAccess():
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': _('htaccess'), 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], _('Rebuilt %s') % _('htaccess'))
+ else:
+ message = _('htaccess regeneration deactivated by sysop.')
+ else:
+ t1 = time.time()
+ board = setBoard(board_dir)
+ regenerateBoard(everything)
+
+ message = _('Rebuilt %(board)s in %(time)s seconds.') % {'board': '/' + board['dir'] + '/', 'time': timeTaken(t1, time.time())}
+ logAction(staff_account['username'], 'Rebuilt /' + board['dir'] + '/')
+
+ template_filename = "message.html"
+ elif path_split[2] == 'mod':
+ if not moderator:
+ return
+
+ try:
+ board = setBoard(path_split[3])
+ except:
+ board = ""
+
+ if not board:
+ template_filename = "mod.html"
+ template_values = {"mode": 1, 'boards': boardlist()}
+ elif self.formdata.get("thread"):
+ parentid = int(self.formdata["thread"])
+ posts = FetchAll('SELECT id, timestamp, timestamp_formatted, name, message, file, thumb, IS_DELETED, locked, subject, length, INET_NTOA(ip) AS ip FROM `posts` WHERE (parentid = %d OR id = %d) AND boardid = %s ORDER BY `id` ASC' % (parentid, parentid, board['id']))
+ template_filename = "mod.html"
+ template_values = {"mode": 3, "dir": board["dir"], "posts": posts}
+ else:
+ threads = FetchAll("SELECT * FROM `posts` WHERE boardid = %s AND parentid = 0 ORDER BY `bumped` DESC" % board["id"])
+ template_filename = "mod.html"
+ template_values = {"mode": 2, "dir": board["dir"], "threads": threads}
+ elif path_split[2] == 'staff':
+ if staff_account['rights'] != '0':
+ return
+ action_taken = False
+
+ if len(path_split) > 3:
+ if path_split[3] == 'add' or path_split[3] == 'edit':
+ member = None
+ member_username = ''
+ member_rights = '3'
+
+ if path_split[3] == 'edit':
+ if len(path_split) > 4:
+ member = FetchOne('SELECT * FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1')
+ if member:
+ member_username = member['username']
+ member_rights = member['rights']
+ action = 'edit/' + member['id']
+
+ try:
+ if self.formdata['username'] != '':
+ if self.formdata['rights'] in ['0', '1', '2', '3']:
+ action_taken = True
+ if not ':' in self.formdata['username']:
+ UpdateDb("UPDATE `staff` SET `username` = '" + _mysql.escape_string(self.formdata['username']) + "', `rights` = " + self.formdata['rights'] + " WHERE `id` = " + member['id'] + " LIMIT 1")
+ message = _('Staff member updated.')
+ logAction(staff_account['username'], _('Updated staff account for %s') % self.formdata['username'])
+ else:
+ message = _('The character : can not be used in usernames.')
+ template_filename = "message.html"
+ except:
+ pass
+ else:
+ action = 'add'
+ try:
+ if self.formdata['username'] != '' and self.formdata['password'] != '':
+ username_taken = FetchOne('SELECT * FROM `staff` WHERE `username` = \'' + _mysql.escape_string(self.formdata['username']) + '\' LIMIT 1')
+ if not username_taken:
+ if self.formdata['rights'] in ['0', '1', '2', '3']:
+ action_taken = True
+ if not ':' in self.formdata['username']:
+ password = genPasswd(self.formdata['password'])
+
+ InsertDb("INSERT INTO `staff` (`username`, `password`, `added`, `rights`) VALUES ('" + _mysql.escape_string(self.formdata['username']) + "', '" + _mysql.escape_string(password) + "', " + str(timestamp()) + ", " + self.formdata['rights'] + ")")
+ message = _('Staff member added.')
+ logAction(staff_account['username'], 'Added staff account for ' + self.formdata['username'])
+ else:
+ message = _('The character : can not be used in usernames.')
+
+ template_filename = "message.html"
+ else:
+ action_taken = True
+ message = _('That username is already in use.')
+ template_filename = "message.html"
+ except:
+ pass
+
+ if not action_taken:
+ action_taken = True
+
+ if action == 'add':
+ submit = 'Agregar'
+ else:
+ submit = 'Editar'
+
+ template_filename = "staff.html"
+ template_values = {'mode': 1,
+ 'action': action,
+ 'member': member,
+ 'member_username': member_username,
+ 'member_rights': member_rights,
+ 'submit': submit}
+ elif path_split[3] == 'delete':
+ if not moderator:
+ return
+
+ action_taken = True
+ message = '<a href="' + Settings.CGI_URL + 'manage/staff/delete_confirmed/' + path_split[4] + '">' + _('Click here to confirm the deletion of that staff member') + '</a>'
+ template_filename = "message.html"
+ elif path_split[3] == 'delete_confirmed':
+ if not moderator:
+ return
+
+ try:
+ action_taken = True
+ member = FetchOne('SELECT `username` FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1')
+ if member:
+ UpdateDb('DELETE FROM `staff` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1')
+ message = 'Staff member deleted.'
+ template_filename = "message.html"
+ logAction(staff_account['username'], _('Deleted staff account for %s') % member['username'])
+ else:
+ message = _('Unable to locate a staff account with that ID.')
+ template_filename = "message.html"
+ except:
+ pass
+
+ if not action_taken:
+ staff = FetchAll('SELECT * FROM `staff` ORDER BY `rights`')
+ for member in staff:
+ if member['rights'] == '0':
+ member ['rights'] = _('Super-administrator')
+ elif member['rights'] == '1':
+ member ['rights'] = _('Administrator')
+ elif member['rights'] == '2':
+ member ['rights'] = _('Developer')
+ elif member['rights'] == '3':
+ member ['rights'] = _('Moderator')
+ if member['lastactive'] != '0':
+ member['lastactivestamp'] = member['lastactive']
+ member['lastactive'] = formatTimestamp(member['lastactive'])
+ else:
+ member['lastactive'] = _('Never')
+ member['lastactivestamp'] = '0'
+ template_filename = "staff.html"
+ template_values = {'mode': 0, 'staff': staff}
+ elif path_split[2] == 'delete':
+ if not moderator:
+ return
+
+ do_ban = False
+ try:
+ if self.formdata['ban'] == 'true':
+ do_ban = True
+ except:
+ pass
+
+ template_filename = "delete.html"
+ template_values = {'do_ban': do_ban, 'curboard': path_split[3], 'postid': path_split[4]}
+ elif path_split[2] == 'delete_confirmed':
+ if not moderator:
+ return
+
+ do_ban = self.formdata.get('ban')
+ permanently = self.formdata.get('perma')
+ imageonly = self.formdata.get('imageonly')
+
+ board = setBoard(path_split[3])
+ postid = int(path_split[4])
+ post = FetchOne('SELECT id, message, parentid, INET_NTOA(ip) AS ip FROM posts WHERE boardid = %s AND id = %s' % (board['id'], postid))
+
+ if not permanently:
+ deletePost(path_split[4], None, '2', imageonly)
+ else:
+ deletePost(path_split[4], None, '0', imageonly)
+ regenerateHome()
+
+ # Borrar denuncias
+ UpdateDb("DELETE FROM `reports` WHERE `postid` = '"+_mysql.escape_string(path_split[4])+"'")
+ boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`')
+
+ if imageonly:
+ message = 'Archivo de post /%s/%s eliminado.' % (board['dir'], post['id'])
+ elif permanently or post["parentid"] == '0':
+ message = 'Post /%s/%s eliminado permanentemente.' % (board['dir'], post['id'])
+ else:
+ message = 'Post /%s/%s enviado a la papelera.' % (board['dir'], post['id'])
+ template_filename = "message.html"
+ logAction(staff_account['username'], message + ' Contenido: ' + post['message'] + ' IP: ' + post['ip'])
+
+ if do_ban:
+ message = _('Redirecting to ban page...') + '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage/ban?ip=' + post['ip'] + '" />'
+ template_filename = "message.html"
+ elif path_split[2] == 'lock':
+ setLocked = 0
+
+ # Nos vamos al board y ubicamos el post
+ board = setBoard(path_split[3])
+ post = FetchOne('SELECT `parentid`, `locked` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1')
+ if not post:
+ message = _('Unable to locate a post with that ID.')
+ template_filename = "message.html"
+ else:
+ if post['parentid'] != '0':
+ message = _('Post is not a thread opener.')
+ template_filename = "message.html"
+ else:
+ if post['locked'] == '0':
+ # Cerrar si esta abierto
+ setLocked = 1
+ else:
+ # Abrir si esta cerrado
+ setLocked = 0
+
+ UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setLocked, board["id"], _mysql.escape_string(path_split[4])))
+ threadUpdated(path_split[4])
+ if setLocked == 1:
+ message = _('Thread successfully closed.')
+ logAction(staff_account['username'], _('Closed thread %s') % ('/' + path_split[3] + '/' + path_split[4]))
+ else:
+ message = _('Thread successfully opened.')
+ logAction(staff_account['username'], _('Opened thread %s') % ('/' + path_split[3] + '/' + path_split[4]))
+ template_filename = "message.html"
+ elif path_split[2] == 'permasage':
+ setPermasaged = 0
+
+ # Nos vamos al board y ubicamos el post
+ board = setBoard(path_split[3])
+ post = FetchOne('SELECT `parentid`, `locked` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1')
+ if not post:
+ message = 'Unable to locate a post with that ID.'
+ template_filename = "message.html"
+ elif post['locked'] == '1':
+ message = 'Solo se puede aplicar permasage en un hilo abierto.'
+ template_filename = "message.html"
+ else:
+ if post['parentid'] != '0':
+ message = 'Post is not a thread opener.'
+ template_filename = "message.html"
+ else:
+ if post['locked'] == '2':
+ # Sacar permasage
+ setPermasaged = 0
+ else:
+ # Colocar permasage
+ setPermasaged = 2
+
+ UpdateDb("UPDATE `posts` SET `locked` = %d WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (setPermasaged, board["id"], _mysql.escape_string(path_split[4])))
+ regenerateFrontPages()
+ threadUpdated(path_split[4])
+
+ if setPermasaged == 2:
+ message = 'Thread successfully permasaged.'
+ logAction(staff_account['username'], 'Enabled permasage in thread /' + path_split[3] + '/' + path_split[4])
+ else:
+ message = 'Thread successfully un-permasaged.'
+ logAction(staff_account['username'], 'Disabled permasage in thread /' + path_split[3] + '/' + path_split[4])
+ template_filename = "message.html"
+ elif path_split[2] == 'move':
+ if not moderator:
+ return
+
+ oldboardid = ""
+ oldthread = ""
+ newboardid = ""
+ try:
+ oldboardid = path_split[3]
+ oldthread = path_split[4]
+ newboardid = path_split[5]
+ except:
+ pass
+
+ try:
+ oldboardid = self.formdata['oldboardid']
+ oldthread = self.formdata['oldthread']
+ newboardid = self.formdata['newboardid']
+ except:
+ pass
+
+ if oldboardid and oldthread and newboardid:
+ message = "import"
+ import shutil
+ message += "ok"
+
+ board = setBoard(oldboardid)
+ oldboard = board['dir']
+ oldboardsubject = board['subject']
+
+ # get old posts
+ posts = FetchAll("SELECT * FROM `posts` WHERE (`id` = {0} OR `parentid` = {0}) AND `boardid` = {1} ORDER BY id ASC".format(oldthread, board['id']))
+
+ # switch to new board
+ board = setBoard(newboardid)
+ newboard = board['dir']
+
+ refs = {}
+ moved_files = []
+ moved_thumbs = []
+ moved_cats = []
+ newthreadid = 0
+ newthread = 0
+ num = 1
+
+ message = "from total: %s<br>" % len(posts)
+ template_filename = "message.html"
+
+ for p in posts:
+ # save old post ID
+ old_id = p['id']
+ is_op = bool(p['parentid'] == '0')
+
+ # copy post object but without ID and target boardid
+ post = Post()
+ post.post = p
+ post.post.pop("id")
+ post["boardid"] = board['id']
+ post["parentid"] = newthreadid
+
+ # save the files we need to move if any
+ if post['IS_DELETED'] == '0':
+ if post['file']:
+ moved_files.append(post['file'])
+ if post['thumb']:
+ moved_thumbs.append(post['thumb'])
+ if is_op:
+ moved_cats.append(post['thumb'])
+
+ # fix subject if necessary
+ if post['subject'] and post['subject'] == oldboardsubject:
+ post['subject'] = board['subject']
+
+ # insert new post and get its new ID
+ new_id = post.insert()
+
+ # save the reference (BBS = post number, IB = new ID)
+ refs[old_id] = num if board['board_type'] == '1' else new_id
+
+ # this was an OP
+ message += "newthread = %s parentid = %s<br>" % (newthreadid, p['parentid'])
+ if is_op:
+ oldthread = old_id
+ newthreadid = new_id
+ oldbumped = post["bumped"]
+
+ # BBS = new thread timestamp, IB = new thread ID
+ newthread = post['timestamp'] if board['board_type'] == '1' else new_id
+
+ # log it
+ message += "%s -> %s<br>" % (old_id, new_id)
+
+ num += 1
+
+ # fix anchors
+ for old, new in refs.iteritems():
+ old_url = "/{oldboard}/res/{oldthread}.html#{oldpost}\">&gt;&gt;{oldpost}</a>".format(oldboard=oldboard, oldthread=oldthread, oldpost=old)
+
+ if board['board_type'] == '1':
+ new_url = "/{newboard}/read/{newthread}/{newpost}\">&gt;&gt;{newpost}</a>".format(newboard=newboard, newthread=newthread, newpost=new)
+ else:
+ new_url = "/{newboard}/res/{newthread}.html#{newpost}\">&gt;&gt;{newpost}</a>".format(newboard=newboard, newthread=newthread, newpost=new)
+
+ sql = "UPDATE `posts` SET `message` = REPLACE(message, '{old}', '{new}') WHERE `boardid` = {newboardid} AND (`id` = {newthreadid} OR `parentid` = {newthreadid})".format(old=old_url, new=new_url, newboardid=board['id'], newthreadid=newthreadid)
+ message += sql + "<br>"
+ UpdateDb(sql)
+
+ # copy files
+ for file in moved_files:
+ if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/src/" + file):
+ shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/src/" + file, Settings.IMAGES_DIR + newboard + "/src/" + file)
+ for thumb in moved_thumbs:
+ if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/thumb/" + thumb):
+ shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/thumb/" + thumb, Settings.IMAGES_DIR + newboard + "/thumb/" + thumb)
+ if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/mobile/" + thumb):
+ shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/mobile/" + thumb, Settings.IMAGES_DIR + newboard + "/mobile/" + thumb)
+ for cat in moved_cats:
+ try:
+ if not os.path.isfile(Settings.IMAGES_DIR + newboard + "/cat/" + thumb):
+ shutil.copyfile(Settings.IMAGES_DIR + oldboard + "/cat/" + thumb, Settings.IMAGES_DIR + newboard + "/cat/" + thumb)
+ except:
+ pass
+
+ # lock original, set expiration to 1 day
+ exp = timestamp()+86400
+ exp_format = datetime.datetime.fromtimestamp(exp).strftime("%d/%m")
+ sql = "UPDATE `posts` SET `locked`=1, `expires`={exp}, `expires_formatted`=\"{exp_format}\" WHERE `boardid`=\"{oldboard}\" AND id=\"{oldthread}\"".format(exp=exp,exp_format=exp_format,oldboard=oldboardid,oldthread=oldthread)
+ UpdateDb(sql)
+
+ # insert notice message
+ if 'msg' in self.formdata:
+ board = setBoard(oldboard)
+
+ if board['board_type'] == '1':
+ thread_url = "/{newboard}/read/{newthread}".format(newboard=newboard, newthread=newthread)
+ else:
+ thread_url = "/{newboard}/res/{newthread}.html".format(newboard=newboard, newthread=newthread)
+
+ notice_post = Post(board["id"])
+ notice_post["parentid"] = oldthread
+ if board['board_type'] == "0":
+ notice_post["subject"] = "Aviso"
+ notice_post["name"] = "Sistema"
+ notice_post["message"] = "El hilo ha sido movido a <a href=\"{url}\">/{newboard}/{newthread}</a>.".format(url=thread_url, newboard=newboard, newthread=newthread)
+ notice_post["timestamp"] = timestamp()+1
+ notice_post["timestamp_formatted"] = "Hilo movido"
+ notice_post["bumped"] = oldbumped
+ notice_post.insert()
+
+ # regenerate
+ regenerateFrontPages()
+ regenerateThreadPage(newthreadid)
+ regenerateThreadPage(oldthread)
+
+ message += "done"
+
+ logAction(staff_account['username'], "Movido hilo %s/%s a %s/%s." % (oldboard, oldthread, newboard, newthread))
+ else:
+ template_filename = "move.html"
+ template_values = {'boards': boardlist(), 'oldboardid': oldboardid, 'oldthread': oldthread}
+ elif path_split[2] == 'ban':
+ if not moderator:
+ return
+
+ if len(path_split) > 4:
+ board = setBoard(path_split[3])
+ post = FetchOne('SELECT `ip` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\' LIMIT 1')
+ formatted_ip = inet_ntoa(long(post['ip']))
+ #Creo que esto no deberia ir aqui... -> UpdateDb('UPDATE `posts` SET `banned` = 1 WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[4]) + '\'')
+ if not post:
+ message = _('Unable to locate a post with that ID.')
+ template_filename = "message.html"
+ else:
+ message = '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage/ban?ip=' + formatted_ip + '" />Espere...'
+ template_filename = "message.html"
+ else:
+ #if path_split[3] == '':
+ try:
+ ip = self.formdata['ip']
+ except:
+ ip = ''
+ try:
+ netmask = insnetmask = self.formdata['netmask']
+ if netmask == '255.255.255.255':
+ insnetmask = ''
+ except:
+ netmask = instnetmask = ''
+ #else:
+ # ip = path_split[3]
+ if ip != '':
+ try:
+ reason = self.formdata['reason']
+ except:
+ reason = None
+ if reason is not None:
+ if self.formdata['seconds'] != '0':
+ until = str(timestamp() + int(self.formdata['seconds']))
+ else:
+ until = '0'
+ where = ''
+ if 'board_all' not in self.formdata.keys():
+ where = []
+ boards = FetchAll('SELECT `dir` FROM `boards`')
+ for board in boards:
+ keyname = 'board_' + board['dir']
+ if keyname in self.formdata.keys():
+ if self.formdata[keyname] == "1":
+ where.append(board['dir'])
+ if len(where) > 0:
+ where = pickle.dumps(where)
+ else:
+ self.error(_("You must select where the ban shall be placed"))
+ return
+
+ if 'edit' in self.formdata.keys():
+ UpdateDb("DELETE FROM `bans` WHERE `id` = '" + _mysql.escape_string(self.formdata['edit']) + "' LIMIT 1")
+ else:
+ ban = FetchOne("SELECT `id` FROM `bans` WHERE `ip` = '" + _mysql.escape_string(ip) + "' AND `boards` = '" + _mysql.escape_string(where) + "' LIMIT 1")
+ if ban:
+ self.error(_('There is already an identical ban for this IP.') + '<a href="'+Settings.CGI_URL+'manage/ban/' + ip + '?edit=' + ban['id']+'">' + _('Edit') + '</a>')
+ return
+
+ # Blind mode
+ if 'blind' in self.formdata.keys() and self.formdata['blind'] == '1':
+ blind = '1'
+ else:
+ blind = '0'
+
+ # Banear sin mensaje
+ InsertDb("INSERT INTO `bans` (`ip`, `netmask`, `boards`, `added`, `until`, `staff`, `reason`, `note`, `blind`) VALUES (INET_ATON('" + _mysql.escape_string(ip) + "') & INET_ATON('"+_mysql.escape_string(netmask)+"'), INET_ATON('"+_mysql.escape_string(insnetmask)+"'), '" + _mysql.escape_string(where) + "', " + str(timestamp()) + ", " + until + ", '" + _mysql.escape_string(staff_account['username']) + "', '" + _mysql.escape_string(self.formdata['reason']) + "', '" + _mysql.escape_string(self.formdata['note']) + "', '"+blind+"')")
+
+ regenerateAccess()
+ if 'edit' in self.formdata.keys():
+ message = _('Ban successfully edited.')
+ action = 'Edited ban for ' + ip
+ else:
+ message = _('Ban successfully placed.')
+ action = 'Banned ' + ip
+ if until != '0':
+ action += ' until ' + formatTimestamp(until)
+ else:
+ action += ' permanently'
+ logAction(staff_account['username'], action)
+ template_filename = 'message.html'
+ else:
+ startvalues = {'where': [],
+ 'netmask': '255.255.255.255',
+ 'reason': '',
+ 'note': '',
+ 'message': '(GET OUT)',
+ 'seconds': '0',
+ 'blind': '1'}
+ edit_id = 0
+ if 'edit' in self.formdata.keys():
+ edit_id = self.formdata['edit']
+ ban = FetchOne("SELECT `id`, INET_NTOA(`ip`) AS 'ip', CASE WHEN `netmask` IS NULL THEN '255.255.255.255' ELSE INET_NTOA(`netmask`) END AS 'netmask', boards, added, until, staff, reason, note, blind FROM `bans` WHERE `id` = '" + _mysql.escape_string(edit_id) + "' ORDER BY `added` DESC")
+ if ban:
+ if ban['boards'] == '':
+ where = ''
+ else:
+ where = pickle.loads(ban['boards'])
+ if ban['until'] == '0':
+ until = 0
+ else:
+ until = int(ban['until']) - timestamp()
+ startvalues = {'where': where,
+ 'netmask': ban['netmask'],
+ 'reason': ban['reason'],
+ 'note': ban['note'],
+ 'seconds': str(until),
+ 'blind': ban['blind']
+ }
+ else:
+ edit_id = 0
+
+ template_filename = "bans.html"
+ template_values = {'mode': 1,
+ 'boards': boardlist(),
+ 'ip': ip,
+ 'startvalues': startvalues,
+ 'edit_id': edit_id}
+ elif path_split[2] == 'bans':
+ if not moderator:
+ return
+
+ action_taken = False
+ if len(path_split) > 4:
+ if path_split[3] == 'delete':
+ ip = FetchOne("SELECT INET_NTOA(`ip`) AS 'ip' FROM `bans` WHERE `id` = '" + _mysql.escape_string(path_split[4]) + "' LIMIT 1", 0)[0]
+ if ip != '':
+ # Delete ban
+ UpdateDb('DELETE FROM `bans` WHERE `id` = ' + _mysql.escape_string(path_split[4]) + ' LIMIT 1')
+ regenerateAccess()
+ message = _('Ban successfully deleted.')
+ template_filename = "message.html"
+ logAction(staff_account['username'], _('Deleted ban for %s') % ip)
+ else:
+ message = _('There was a problem while deleting that ban. It may have already been removed, or recently expired.')
+ template_filename = "message.html"
+
+ if not action_taken:
+ bans = FetchAll("SELECT `id`, INET_NTOA(`ip`) AS 'ip', CASE WHEN `netmask` IS NULL THEN '255.255.255.255' ELSE INET_NTOA(`netmask`) END AS 'netmask', boards, added, until, staff, reason, note, blind FROM `bans` ORDER BY `added` DESC")
+ if bans:
+ for ban in bans:
+ if ban['boards'] == '':
+ ban['boards'] = _('All boards')
+ else:
+ where = pickle.loads(ban['boards'])
+ if len(where) > 1:
+ ban['boards'] = '/' + '/, /'.join(where) + '/'
+ else:
+ ban['boards'] = '/' + where[0] + '/'
+ ban['added'] = formatTimestamp(ban['added'])
+ if ban['until'] == '0':
+ ban['until'] = _('Does not expire')
+ else:
+ ban['until'] = formatTimestamp(ban['until'])
+ if ban['blind'] == '1':
+ ban['blind'] = 'Sí'
+ else:
+ ban['blind'] = 'No'
+ template_filename = "bans.html"
+ template_values = {'mode': 0, 'bans': bans}
+ elif path_split[2] == 'changepassword':
+ form_submitted = False
+ try:
+ if self.formdata['oldpassword'] != '' and self.formdata['newpassword'] != '' and self.formdata['newpassword2'] != '':
+ form_submitted = True
+ except:
+ pass
+ if form_submitted:
+ if genPasswd(self.formdata['oldpassword']) == staff_account['password']:
+ if self.formdata['newpassword'] == self.formdata['newpassword2']:
+ UpdateDb('UPDATE `staff` SET `password` = \'' + genPasswd(self.formdata['newpassword']) + '\' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1')
+ message = _('Password successfully changed. Please log out and log back in.')
+ template_filename = "message.html"
+ else:
+ message = _('Passwords did not match.')
+ template_filename = "message.html"
+ else:
+ message = _('Current password incorrect.')
+ template_filename = "message.html"
+ else:
+ template_filename = "changepassword.html"
+ template_values = {}
+ elif path_split[2] == 'board':
+ if not administrator:
+ return
+
+ if len(path_split) > 3:
+ board = setBoard(path_split[3])
+ form_submitted = False
+ try:
+ if self.formdata['name'] != '':
+ form_submitted = True
+ except:
+ pass
+ if form_submitted:
+ # Update board settings
+ board['name'] = self.formdata['name']
+ board['longname'] = self.formdata['longname']
+ board['subname'] = self.formdata['subname']
+ board['anonymous'] = self.formdata['anonymous']
+ board['subject'] = self.formdata['subject']
+ board['message'] = self.formdata['message']
+ if board['dir'] != 'anarkia':
+ board['board_type'] = self.formdata['type']
+ board['useid'] = self.formdata['useid']
+ board['slip'] = self.formdata['slip']
+ board['countrycode'] = self.formdata['countrycode']
+ if 'recyclebin' in self.formdata.keys():
+ board['recyclebin'] = '1'
+ else:
+ board['recyclebin'] = '0'
+ if 'disable_name' in self.formdata.keys():
+ board['disable_name'] = '1'
+ else:
+ board['disable_name'] = '0'
+ if 'disable_subject' in self.formdata.keys():
+ board['disable_subject'] = '1'
+ else:
+ board['disable_subject'] = '0'
+ if 'secret' in self.formdata.keys():
+ board['secret'] = '1'
+ else:
+ board['secret'] = '0'
+ if 'locked' in self.formdata.keys():
+ board['locked'] = '1'
+ else:
+ board['locked'] = '0'
+ board['postarea_desc'] = self.formdata['postarea_desc']
+ if 'allow_noimage' in self.formdata.keys():
+ board['allow_noimage'] = '1'
+ else:
+ board['allow_noimage'] = '0'
+ if 'allow_images' in self.formdata.keys():
+ board['allow_images'] = '1'
+ else:
+ board['allow_images'] = '0'
+ if 'allow_image_replies' in self.formdata.keys():
+ board['allow_image_replies'] = '1'
+ else:
+ board['allow_image_replies'] = '0'
+ if 'allow_spoilers' in self.formdata.keys():
+ board['allow_spoilers'] = '1'
+ else:
+ board['allow_spoilers'] = '0'
+ if 'allow_oekaki' in self.formdata.keys():
+ board['allow_oekaki'] = '1'
+ else:
+ board['allow_oekaki'] = '0'
+ if 'archive' in self.formdata.keys():
+ board['archive'] = '1'
+ else:
+ board['archive'] = '0'
+ board['postarea_extra'] = self.formdata['postarea_extra']
+ board['force_css'] = self.formdata['force_css']
+
+ # Update file types
+ UpdateDb("DELETE FROM `boards_filetypes` WHERE `boardid` = %s" % board['id'])
+ for filetype in filetypelist():
+ if 'filetype'+filetype['ext'] in self.formdata.keys():
+ UpdateDb("INSERT INTO `boards_filetypes` VALUES (%s, %s)" % (board['id'], filetype['id']))
+
+ try:
+ board['numthreads'] = int(self.formdata['numthreads'])
+ except:
+ raise UserError, _("Max threads shown must be numeric.")
+
+ try:
+ board['numcont'] = int(self.formdata['numcont'])
+ except:
+ raise UserError, _("Max replies shown must be numeric.")
+
+ try:
+ board['numline'] = int(self.formdata['numline'])
+ except:
+ raise UserError, _("Max lines shown must be numeric.")
+
+ try:
+ board['thumb_px'] = int(self.formdata['thumb_px'])
+ except:
+ raise UserError, _("Max thumb dimensions must be numeric.")
+
+ try:
+ board['maxsize'] = int(self.formdata['maxsize'])
+ except:
+ raise UserError, _("Max size must be numeric.")
+
+ try:
+ board['maxage'] = int(self.formdata['maxage'])
+ except:
+ raise UserError, _("Max age must be numeric.")
+
+ try:
+ board['maxinactive'] = int(self.formdata['maxinactive'])
+ except:
+ raise UserError, _("Max inactivity must be numeric.")
+
+ try:
+ board['threadsecs'] = int(self.formdata['threadsecs'])
+ except:
+ raise UserError, _("Time between new threads must be numeric.")
+
+ try:
+ board['postsecs'] = int(self.formdata['postsecs'])
+ except:
+ raise UserError, _("Time between replies must be numeric.")
+
+ updateBoardSettings()
+ message = _('Board options successfully updated.') + ' <a href="'+Settings.CGI_URL+'manage/rebuild/'+board['dir']+'">'+_('Rebuild')+'</a>'
+ template_filename = "message.html"
+ logAction(staff_account['username'], _('Updated options for /%s/') % board['dir'])
+ else:
+ template_filename = "boardoptions.html"
+ template_values = {'mode': 1, 'boardopts': board, 'filetypes': filetypelist(), 'supported_filetypes': board['filetypes_ext']}
+ else:
+ # List all boards
+ template_filename = "boardoptions.html"
+ template_values = {'mode': 0, 'boards': boardlist()}
+ elif path_split[2] == 'recyclebin':
+ if not administrator:
+ return
+
+ message = None
+ if len(path_split) > 5:
+ if path_split[4] == 'restore':
+ board = setBoard(path_split[5])
+
+ post = FetchOne('SELECT `parentid` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1')
+ if not post:
+ message = _('Unable to locate a post with that ID.') + '<br />'
+ template_filename = "message.html"
+ else:
+ UpdateDb('UPDATE `posts` SET `IS_DELETED` = 0 WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1')
+ if post['parentid'] != '0':
+ threadUpdated(post['parentid'])
+ else:
+ regenerateFrontPages()
+
+ message = _('Post successfully restored.')
+ logAction(staff_account['username'], _('Restored post %s') % ('/' + path_split[5] + '/' + path_split[6]))
+
+ if path_split[4] == 'delete':
+ board = setBoard(path_split[5])
+ post = FetchOne('SELECT `parentid` FROM `posts` WHERE `boardid` = ' + board['id'] + ' AND `id` = \'' + _mysql.escape_string(path_split[6]) + '\' LIMIT 1')
+ if not post:
+ message = _('Unable to locate a post with that ID.')
+ else:
+ deletePost(path_split[6], None)
+
+ if post['parentid'] != '0':
+ threadUpdated(post['parentid'])
+ else:
+ regenerateFrontPages()
+
+ message = _('Post successfully permadeleted.')
+ logAction(staff_account['username'], _('Permadeleted post %s') % ('/' + path_split[5] + '/' + path_split[6]))
+
+ # Delete more than 1 post
+ if 'deleteall' in self.formdata.keys():
+ return # TODO
+ deleted = 0
+ for key in self.formdata.keys():
+ if key[:2] == '!i':
+ dir = key[2:].split('/')[0] # Board where the post is
+ postid = key[2:].split('/')[1] # Post to delete
+
+ # Delete post start
+ post = FetchOne('SELECT `parentid`, `dir` FROM `posts` INNER JOIN `boards` ON posts.boardid = boards.id WHERE `dir` = \'' + _mysql.escape_string(dir) + '\' AND posts.id = \'' + _mysql.escape_string(postid) + '\' LIMIT 1')
+ if not post:
+ message = _('Unable to locate a post with that ID.')
+ else:
+ board = setBoard(dir)
+ deletePost(int(postid), None)
+ if post['parentid'] != '0':
+ threadUpdated(post['parentid'])
+ else:
+ regenerateFrontPages()
+ deleted += 1
+ # Delete post end
+
+ logAction(staff_account['username'], _('Permadeleted %s post(s).') % str(deleted))
+ message = _('Permadeleted %s post(s).') % str(deleted)
+
+ ## Start
+ import math
+ pagesize = float(Settings.RECYCLEBIN_POSTS_PER_PAGE)
+
+ try:
+ currentpage = int(path_split[3])
+ except:
+ currentpage = 0
+
+ skip = False
+ if 'type' in self.formdata.keys():
+ type = int(self.formdata["type"])
+ else:
+ type = 0
+
+ # Generate board list
+ boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`')
+ for board in boards:
+ if 'board' in self.formdata.keys() and self.formdata['board'] == board['dir']:
+ board['checked'] = True
+ else:
+ board['checked'] = False
+
+ # Get type filter
+ if type != 0:
+ type_condition = "= " + str(type)
+ else:
+ type_condition = "!= 0"
+
+ # Table
+ if 'board' in self.formdata.keys() and self.formdata['board'] != 'all':
+ cboard = self.formdata['board']
+ posts = FetchAll("SELECT posts.id, posts.timestamp, timestamp_formatted, IS_DELETED, INET_NTOA(posts.ip) as ip, posts.message, dir, boardid FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE `dir` = '%s' AND IS_DELETED %s ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), _mysql.escape_string(type_condition), currentpage*pagesize, pagesize))
+ try:
+ totals = FetchOne("SELECT COUNT(id) FROM `posts` WHERE IS_DELETED %s AND `boardid` = %s" % (_mysql.escape_string(type_condition), _mysql.escape_string(posts[0]['boardid'])), 0)
+ except:
+ skip = True
+ else:
+ cboard = 'all'
+ posts = FetchAll("SELECT posts.id, posts.timestamp, timestamp_formatted, IS_DELETED, posts.ip, posts.message, dir FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE IS_DELETED %s ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(type_condition), currentpage*pagesize, pagesize))
+ totals = FetchOne("SELECT COUNT(id) FROM `posts` WHERE IS_DELETED %s" % _mysql.escape_string(type_condition), 0)
+
+ template_filename = "recyclebin.html"
+ template_values = {'message': message,
+ 'type': type,
+ 'boards': boards,
+ 'skip': skip}
+
+ if not skip:
+ # Calculate number of pages
+ total = int(totals[0])
+ pages = int(math.ceil(total / pagesize))
+
+ # Create delete form
+ if 'board' in self.formdata.keys():
+ board = self.formdata['board']
+ else:
+ board = None
+
+ navigator = ''
+ if currentpage > 0:
+ navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(currentpage-1)+'?type='+str(type)+'&amp;board='+cboard+'">&lt;</a> '
+ else:
+ navigator += '&lt; '
+
+ for i in range(pages):
+ if i != currentpage:
+ navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(i)+'?type='+str(type)+'&amp;board='+cboard+'">'+str(i)+'</a> '
+ else:
+ navigator += str(i)+' '
+
+ if currentpage < (pages-1):
+ navigator += '<a href="'+Settings.CGI_URL+'manage/recyclebin/'+str(currentpage+1)+'?type='+str(type)+'&amp;board='+cboard+'">&gt;</a> '
+ else:
+ navigator += '&gt; '
+
+ template_values.update({'currentpage': currentpage,
+ 'curboard': board,
+ 'posts': posts,
+ 'navigator': navigator})
+ # End recyclebin
+ elif path_split[2] == 'lockboard':
+ if not administrator:
+ return
+
+ try:
+ board_dir = path_split[3]
+ except:
+ board_dir = ''
+
+ if board_dir == '':
+ template_filename = "lockboard.html"
+ template_values = {'boards': boardlist()}
+ elif path_split[2] == 'boardlock':
+ board = setBoard(path_split[3])
+ if int(board['locked']):
+ # Si esta cerrado... abrir
+ board['locked'] = 0
+ updateBoardSettings()
+ message = _('Board opened successfully.')
+ template_filename = "message.html"
+ else:
+ # Si esta abierta, cerrar
+ board['locked'] = 1
+ updateBoardSettings()
+ message = _('Board closed successfully.')
+ template_filename = "message.html"
+ elif path_split[2] == 'addboard':
+ if not administrator:
+ return
+
+ action_taken = False
+ board_dir = ''
+
+ try:
+ if self.formdata['name'] != '':
+ board_dir = self.formdata['dir']
+ except:
+ pass
+
+ if board_dir != '':
+ action_taken = True
+ board_exists = FetchOne('SELECT * FROM `boards` WHERE `dir` = \'' + _mysql.escape_string(board_dir) + '\' LIMIT 1')
+ if not board_exists:
+ os.mkdir(Settings.ROOT_DIR + board_dir)
+ os.mkdir(Settings.ROOT_DIR + board_dir + '/res')
+ if not os.path.exists(Settings.IMAGES_DIR + board_dir):
+ os.mkdir(Settings.IMAGES_DIR + board_dir)
+ os.mkdir(Settings.IMAGES_DIR + board_dir + '/src')
+ os.mkdir(Settings.IMAGES_DIR + board_dir + '/thumb')
+ os.mkdir(Settings.IMAGES_DIR + board_dir + '/mobile')
+ os.mkdir(Settings.IMAGES_DIR + board_dir + '/cat')
+ if os.path.exists(Settings.ROOT_DIR + board_dir) and os.path.isdir(Settings.ROOT_DIR + board_dir):
+ UpdateDb('INSERT INTO `boards` (`dir`, `name`) VALUES (\'' + _mysql.escape_string(board_dir) + '\', \'' + _mysql.escape_string(self.formdata['name']) + '\')')
+ board = setBoard(board_dir)
+ f = open(Settings.ROOT_DIR + board['dir'] + '/.htaccess', 'w')
+ try:
+ f.write('DirectoryIndex index.html')
+ finally:
+ f.close()
+ regenerateFrontPages()
+ message = _('Board added')
+ template_filename = "message.html"
+ logAction(staff_account['username'], _('Added board %s') % ('/' + board['dir'] + '/'))
+ else:
+ message = _('There was a problem while making the directories.')
+ template_filename = "message.html"
+ else:
+ message = _('There is already a board with that directory.')
+ template_filename = "message.html"
+
+ if not action_taken:
+ template_filename = "addboard.html"
+ template_values = {}
+ elif path_split[2] == 'trim':
+ if not administrator:
+ return
+ board = setBoard(path_split[3])
+ trimThreads()
+ self.output = "done trimming"
+ return
+ elif path_split[2] == 'setexpires':
+ board = setBoard(path_split[3])
+ parentid = int(path_split[4])
+ days = int(path_split[5])
+ t = time.time()
+
+ expires = int(t) + (days * 86400)
+ date_format = '%d/%m'
+ expires_formatted = datetime.datetime.fromtimestamp(expires).strftime(date_format)
+
+ sql = "UPDATE posts SET expires = timestamp + (%s * 86400), expires_formatted = FROM_UNIXTIME((timestamp + (%s * 86400)), '%s') WHERE boardid = %s AND id = %s" % (str(days), str(days), date_format, board["id"], str(parentid))
+ UpdateDb(sql)
+
+ self.output = "done " + sql
+ return
+ elif path_split[2] == 'fixflood':
+ if not administrator:
+ return
+ board = setBoard('zonavip')
+ threads = FetchAll("SELECT * FROM posts WHERE boardid = %s AND parentid = 0 AND subject LIKE 'querido mod%%'" % board['id'])
+ for thread in threads:
+ self.output += "%s<br>" % thread['id']
+ #deletePost(thread['id'], None)
+ return
+ elif path_split[2] == 'fixico':
+ board = setBoard(path_split[3])
+
+ threads = FetchAll("SELECT * FROM posts WHERE boardid = %s AND parentid = 0 AND message NOT LIKE '<img%%'" % board['id'])
+ for t in threads:
+ img_src = '<img src="%s" alt="ico" /><br />' % getRandomIco()
+ newmessage = img_src + t["message"]
+ #UpdateDb("UPDATE posts SET message = '%s' WHERE boardid = %s AND id = %s" % (_mysql.escape_string(newmessage), board['id'], t['id']))
+
+ self.output = repr(threads)
+ return
+ elif path_split[2] == 'fixkako':
+ board = setBoard(path_split[3])
+
+ threads = FetchAll('SELECT * FROM archive WHERE boardid = %s ORDER BY timestamp DESC' % board['id'])
+ for item in threads:
+ t = time.time()
+ self.output += item['timestamp'] + '<br />'
+ fname = Settings.ROOT_DIR + board["dir"] + "/kako/" + str(item["timestamp"]) + ".json"
+ if os.path.isfile(fname):
+ import json
+ with open(fname) as f:
+ thread = json.load(f)
+ thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']]
+ template_fname = "txt_archive.html"
+
+ post_preview = cut_home_msg(thread['posts'][0]['message'], 0)
+ page = renderTemplate("txt_archive.html", {"threads": [thread], "preview": post_preview}, False)
+ with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".html", "w") as f:
+ f.write(page)
+
+ self.output += 'done' + str(time.time() - t) + '<br />'
+ else:
+ self.output += 'El hilo no existe.<br />'
+ elif path_split[2] == 'fixexpires':
+ board = setBoard(path_split[3])
+
+ if int(board["maxage"]):
+ date_format = '%d/%m'
+ date_format_y = '%m/%Y'
+ if int(board["maxage"]) >= 365:
+ date_format = date_format_y
+ sql = "UPDATE posts SET expires = timestamp + (%s * 86400), expires_formatted = FROM_UNIXTIME((timestamp + (%s * 86400)), '%s') WHERE boardid = %s AND parentid = 0" % (board["maxage"], board["maxage"], date_format, board["id"])
+ UpdateDb(sql)
+
+ alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT))
+ sql = "UPDATE posts SET expires_alert = CASE WHEN UNIX_TIMESTAMP() > (expires - %d*86400) THEN 1 ELSE 0 END WHERE boardid = %s AND parentid = 0" % (alert_time, board["id"])
+ UpdateDb(sql)
+ else:
+ sql = "UPDATE posts SET expires = 0, expires_formatted = '', expires_alert = 0 WHERE boardid = %s AND parentid = 0" % (board["id"])
+ UpdateDb(sql)
+
+ self.output = "done"
+ return
+ elif path_split[2] == 'fixid':
+ board = setBoard(path_split[3])
+ posts = FetchAll('SELECT * FROM `posts` WHERE `boardid` = %s' % board['id'])
+ self.output = "total: %d<br />" % len(posts)
+ for post in posts:
+ new_timestamp_formatted = formatTimestamp(post['timestamp'])
+ tim = 0
+ if board["useid"] != '0':
+ new_timestamp_formatted += ' ID:' + iphash(post['ip'], '', tim, '1', False, False, False, '0')
+ self.output += "%s - %s <br />" % (post['id'], new_timestamp_formatted)
+ query = "UPDATE `posts` SET timestamp_formatted = '%s' WHERE boardid = '%s' AND id = '%s'" % (new_timestamp_formatted, board['id'], post['id'])
+ UpdateDb(query)
+ return
+ elif path_split[2] == 'fixname':
+ board = setBoard(path_split[3])
+ #posts = FetchAll('SELECT * FROM `posts` WHERE `boardid` = %s' % board['id'])
+ posts = FetchAll('SELECT * FROM `posts` WHERE `name` LIKE \'%s\'' % '%%')
+ new_name = board['anonymous']
+ self.output = new_name + "<br />"
+ for post in posts:
+ self.output += "%s<br />" % (post['id'])
+ query = "UPDATE `posts` SET `name` = '%s' WHERE boardid = '%s' AND id = '%s'" % (new_name, board['id'], post['id'])
+ UpdateDb(query)
+ return
+ elif path_split[2] == 'setsub':
+ board = setBoard(path_split[3])
+ thread = FetchOne('SELECT * FROM `posts` WHERE `parentid` = 0 AND `boardid` = %s' % board['id'])
+ subject = str(path_split[4])
+ self.output = subject + "->" + thread['id'] + "<br />"
+ query = "UPDATE `posts` SET `subject` = '%s' WHERE boardid = '%s' AND id = '%s'" % (subject, board['id'], thread['id'])
+ UpdateDb(query)
+ return
+ elif path_split[2] == 'fixlength':
+ board = setBoard(path_split[3])
+ threads = FetchAll('SELECT * FROM `posts` WHERE parentid = 0 AND `boardid` = %s' % board['id'])
+ for t in threads:
+ length = threadNumReplies(t['id'])
+ UpdateDb('UPDATE posts SET length = %d WHERE boardid = %s AND id = %s' % (length, board['id'], t['id']))
+
+ self.output='done'
+ return
+ elif path_split[2] == 'archive':
+ t = time.time()
+ board = setBoard(path_split[3])
+ postid = int(path_split[4])
+ archiveThread(postid)
+ self.output = "todo ok %s" % str(time.time() - t)
+ elif path_split[2] == 'filters':
+ action_taken = False
+ if len(path_split) > 3 and path_split[3] == 'add':
+ if "add" in self.formdata.keys():
+ edit_id = 0
+ if 'edit' in self.formdata.keys():
+ edit_id = int(self.formdata['edit'])
+
+ # We decide what type of filter it is.
+ # 0: Word / 1: Name/Trip
+ filter_type = int(self.formdata["type"])
+ filter_action = int(self.formdata["action"])
+ filter_from = ''
+ filter_tripcode = ''
+
+ # I don't like pickles... oh well.
+ where = ''
+ if 'board_all' not in self.formdata.keys():
+ where = []
+ boards = FetchAll('SELECT `dir` FROM `boards`')
+ for board in boards:
+ keyname = 'board_' + board['dir']
+ if keyname in self.formdata.keys():
+ if self.formdata[keyname] == "1":
+ where.append(board['dir'])
+ if len(where) > 0:
+ where = _mysql.escape_string(pickle.dumps(where))
+ else:
+ self.error(_("You must select what board the filter will affect"))
+ return
+
+ if filter_type == 0:
+ # Word filter
+ if len(self.formdata["word"]) > 0:
+ filter_from = _mysql.escape_string(cgi.escape(self.formdata["word"]))
+ else:
+ self.error(_("You must enter a word."))
+ return
+ elif filter_type == 1:
+ # Name/trip filter
+ can_add = False
+ if len(self.formdata["name"]) > 0:
+ filter_from = _mysql.escape_string(self.formdata["name"])
+ can_add = True
+ if len(self.formdata["trip"]) > 0:
+ filter_tripcode = _mysql.escape_string(self.formdata["trip"])
+ can_add = True
+ if not can_add:
+ self.error(_("You must enter a name and/or a tripcode."))
+ return
+
+ # Action
+ sql_query = ''
+ filter_reason = ''
+ if len(self.formdata["reason"]) > 0:
+ filter_reason = _mysql.escape_string(self.formdata["reason"])
+ if filter_action == 0:
+ # Cancel post
+ sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \
+ (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, str(timestamp()), _mysql.escape_string(staff_account['username']))
+ elif filter_action == 1:
+ # Change to
+ if len(self.formdata["changeto"]) > 0:
+ filter_to = _mysql.escape_string(self.formdata["changeto"])
+ sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `to`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \
+ (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, filter_to, str(timestamp()), _mysql.escape_string(staff_account['username']))
+ else:
+ self.error(_("You must enter a word to change to."))
+ return
+ elif filter_action == 2:
+ # Ban
+ filter_seconds = '0'
+ if len(self.formdata["seconds"]) > 0:
+ filter_seconds = _mysql.escape_string(self.formdata["seconds"])
+ if "blind" in self.formdata.keys() and self.formdata["blind"] == '1':
+ filter_blind = '1'
+ else:
+ filter_blind = '2'
+
+ sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `seconds`, `blind`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \
+ (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, filter_seconds, filter_blind, str(timestamp()), _mysql.escape_string(staff_account['username']))
+ elif filter_action == 3:
+ # Redirect URL
+ if len(self.formdata['redirect_url']) > 0:
+ redirect_url = _mysql.escape_string(self.formdata['redirect_url'])
+ redirect_time = 0
+ try:
+ redirect_time = int(self.formdata['redirect_time'])
+ except:
+ pass
+ else:
+ self.error(_("You must enter a URL to redirect to."))
+ return
+
+ sql_query = "INSERT INTO `filters` (`id`, `boards`, `type`, `action`, `from`, `from_trip`, `reason`, `redirect_url`, `redirect_time`, `added`, `staff`) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \
+ (edit_id, where, str(filter_type), str(filter_action), filter_from, filter_tripcode, filter_reason, redirect_url, str(redirect_time), str(timestamp()), _mysql.escape_string(staff_account['username']))
+ # DO QUERY!
+ if edit_id > 0:
+ UpdateDb("DELETE FROM `filters` WHERE `id` = %s" % str(edit_id))
+ UpdateDb(sql_query)
+ message = 'Filter edited.'
+ else:
+ filt = FetchOne("SELECT `id` FROM `filters` WHERE `boards` = '%s' AND `type` = '%s' AND `from` = '%s'" % (where, str(filter_type), filter_from))
+ if not filt:
+ UpdateDb(sql_query)
+ message = 'Filter added.'
+ else:
+ message = 'This filter already exists here:' + ' <a href="'+Settings.CGI_URL+'manage/filters/add?edit='+filt['id']+'">edit</a>'
+ action_taken = True
+ template_filename = "message.html"
+ else:
+ # Create add form
+ edit_id = 0
+ if 'edit' in self.formdata.keys() and int(self.formdata['edit']) > 0:
+ # Load values
+ edit_id = int(self.formdata['edit'])
+ filt = FetchOne("SELECT * FROM `filters` WHERE `id` = %s LIMIT 1" % str(edit_id))
+ if filt['boards'] == '':
+ where = ''
+ else:
+ where = pickle.loads(filt['boards'])
+ startvalues = {'type': filt['type'],
+ 'trip': filt['from_trip'],
+ 'where': where,
+ 'action': filt['action'],
+ 'changeto': cgi.escape(filt['to'], True),
+ 'reason': filt['reason'],
+ 'seconds': filt['seconds'],
+ 'blind': filt['blind'],
+ 'redirect_url': filt['redirect_url'],
+ 'redirect_time': filt['redirect_time'],}
+ if filt['type'] == '1':
+ startvalues['name'] = filt['from']
+ startvalues['word'] = ''
+ else:
+ startvalues['name'] = ''
+ startvalues['word'] = filt['from']
+ else:
+ startvalues = {'type': '0',
+ 'word': '',
+ 'name': '',
+ 'trip': '',
+ 'where': [],
+ 'action': '0',
+ 'changeto': '',
+ 'reason': _('Forbidden word'),
+ 'seconds': '0',
+ 'blind': '1',
+ 'redirect_url': 'http://',
+ 'redirect_time': '5'}
+
+ if edit_id > 0:
+ submit = "Editar Filtro"
+ else:
+ submit = "Agregar filtro"
+
+ action_taken = True
+ template_filename = "filters.html"
+ template_values = {'mode': 1,
+ 'edit_id': edit_id,
+ 'boards': boardlist(),
+ 'startvalues': startvalues,
+ 'submit': submit}
+ elif len(path_split) > 4 and path_split[3] == 'delete':
+ delid = int(path_split[4])
+ UpdateDb("DELETE FROM `filters` WHERE id = '%s' LIMIT 1" % str(delid))
+ message = _('Deleted filter %s.') % str(delid)
+ template_filename = "message.html"
+ action_taken = True
+
+ if not action_taken:
+ filters = FetchAll("SELECT * FROM `filters` ORDER BY `added` DESC")
+ for filter in filters:
+ if filter['boards'] == '':
+ filter['boards'] = _('All boards')
+ else:
+ where = pickle.loads(filter['boards'])
+ if len(where) > 1:
+ filter['boards'] = '/' + '/, /'.join(where) + '/'
+ else:
+ filter['boards'] = '/' + where[0] + '/'
+ if filter['type'] == '0':
+ filter['type_formatted'] = _('Word:') + ' <b>' + cgi.escape(filter['from']) + '</b>'
+ elif filter['type'] == '1':
+ filter['type_formatted'] = _('Name/Tripcode:')+' '
+ if filter['from'] != '':
+ filter['type_formatted'] += '<b class="name">' + filter['from'] + '</b>'
+ if filter['from_trip'] != '':
+ filter['type_formatted'] += '<span class="trip">' + filter['from_trip'] + '</span>'
+ else:
+ filter['type_formatted'] = '?'
+ if filter['action'] == '0':
+ filter ['action_formatted'] = _('Abort post')
+ elif filter['action'] == '1':
+ filter ['action_formatted'] = _('Change to:') + ' <b>' + cgi.escape(filter['to']) + '</b>'
+ elif filter['action'] == '2':
+ if filter['blind'] == '1':
+ blind = _('Yes')
+ else:
+ blind = _('No')
+ filter ['action_formatted'] = _('Autoban:') + '<br />' + \
+ (_('Length:')+' <i>%s</i><br />'+_('Blind:')+' <i>%s</i>') % (filter['seconds'], blind)
+ elif filter['action'] == '3':
+ filter ['action_formatted'] = (_('Redirect to:')+' %s ('+_('in %s secs')+')') % (filter['redirect_url'], filter['redirect_time'])
+ else:
+ filter ['action_formatted'] = '?'
+ filter['added'] = formatTimestamp(filter['added'])
+
+ template_filename = "filters.html"
+ template_values = {'mode': 0, 'filters': filters}
+ elif path_split[2] == 'logs':
+ if staff_account['rights'] != '0' and staff_account['rights'] != '2':
+ return
+
+ logs = FetchAll('SELECT * FROM `logs` ORDER BY `timestamp` DESC')
+ for log in logs:
+ log['timestamp_formatted'] = formatTimestamp(log['timestamp'])
+ template_filename = "logs.html"
+ template_values = {'logs': logs}
+ elif path_split[2] == 'logout':
+ message = _('Logging out...') + '<meta http-equiv="refresh" content="0;url=' + Settings.CGI_URL + 'manage" />'
+ setCookie(self, 'weabot_manage', '', domain='THIS')
+ setCookie(self, 'weabot_staff', '')
+ template_filename = "message.html"
+ elif path_split[2] == 'quotes':
+ # Quotes for the post screen
+ if "save" in self.formdata.keys():
+ try:
+ f = open('quotes.conf', 'w')
+ f.write(self.formdata["data"])
+ f.close()
+ message = 'Datos guardados.'
+ template_filename = "message.html"
+ except:
+ message = 'Error al guardar datos.'
+ template_filename = "message.html"
+ try:
+ f = open('quotes.conf', 'r')
+ data = cgi.escape(f.read(1048576), True)
+ f.close()
+ template_filename = "quotes.html"
+ template_values = {'data': data}
+ except:
+ message = 'Error al leer datos.'
+ template_filename = 'message.html'
+ elif path_split[2] == 'recent_images':
+ try:
+ if int(self.formdata['images']) > 100:
+ images = '100'
+ else:
+ images = self.formdata['images']
+ posts = FetchAll('SELECT * FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE CHAR_LENGTH(`thumb`) > 0 ORDER BY `timestamp` DESC LIMIT ' + _mysql.escape_string(images))
+ except:
+ posts = FetchAll('SELECT * FROM `posts` INNER JOIN `boards` ON boardid = boards.id WHERE CHAR_LENGTH(`thumb`) > 0 ORDER BY `timestamp` DESC LIMIT 10')
+ template_filename = "recent_images.html"
+ template_values = {'posts': posts}
+ elif path_split[2] == 'news':
+ if not administrator:
+ return
+
+ type = 1
+ if 'type' in self.formdata:
+ type = int(self.formdata['type'])
+
+ if type > 2:
+ raise UserError, "Tipo no soportado"
+
+ # canal del home
+ if len(path_split) > 3:
+ if path_split[3] == 'add':
+ t = datetime.datetime.now()
+
+ # Insertar el nuevo post
+ title = ''
+ message = self.formdata["message"].replace("\n", "<br />")
+
+ # Titulo
+ if 'title' in self.formdata:
+ title = self.formdata["title"]
+
+ # Post anonimo
+ if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1':
+ to_name = "Staff ★"
+ else:
+ to_name = "%s ★" % staff_account['username']
+ timestamp_formatted = formatDate(t)
+ if type > 0:
+ timestamp_formatted = re.sub(r"\(.+", "", timestamp_formatted)
+ else:
+ timestamp_formatted = re.sub(r"\(...\)", " ", timestamp_formatted)
+
+ UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % (type, staff_account['id'], staff_account['username'], _mysql.escape_string(title), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted))
+
+ regenerateNews()
+ regenerateHome()
+ message = _("Added successfully.")
+ template_filename = "message.html"
+ if path_split[3] == 'delete':
+ # Eliminar un post
+ id = int(path_split[4])
+ UpdateDb("DELETE FROM `news` WHERE id = %d AND type = %d" % (id, type))
+ regenerateNews()
+ regenerateHome()
+ message = _("Deleted successfully.")
+ template_filename = "message.html"
+ else:
+ posts = FetchAll("SELECT * FROM `news` WHERE type = %d ORDER BY `timestamp` DESC" % type)
+ template_filename = "news.html"
+ template_values = {'action': type, 'posts': posts}
+ elif path_split[2] == 'newschannel':
+ #if not administrator:
+ # return
+
+ if len(path_split) > 3:
+ if path_split[3] == 'add':
+ t = datetime.datetime.now()
+ # Delete old posts
+ #posts = FetchAll("SELECT `id` FROM `news` WHERE `type` = '1' ORDER BY `timestamp` DESC LIMIT "+str(Settings.MODNEWS_MAX_POSTS)+",30")
+ #for post in posts:
+ # UpdateDb("DELETE FROM `news` WHERE id = " + post['id'] + " AND `type` = '0'")
+
+ # Insert new post
+ message = ''
+ try:
+ # Cut long lines
+ message = self.formdata["message"]
+ message = clickableURLs(cgi.escape(message).rstrip()[0:8000])
+ message = onlyAllowedHTML(message)
+ if Settings.USE_MARKDOWN:
+ message = markdown(message)
+ if not Settings.USE_MARKDOWN:
+ message = message.replace("\n", "<br />")
+ except:
+ pass
+
+ # If it's preferred to remain anonymous...
+ if 'anonymous' in self.formdata.keys() and self.formdata['anonymous'] == '1':
+ to_name = "Staff ★"
+ else:
+ to_name = "%s ★" % staff_account['username']
+ timestamp_formatted = formatDate(t)
+
+ UpdateDb("INSERT INTO `news` (type, staffid, staff_name, title, message, name, timestamp, timestamp_formatted) VALUES ('0', '%s', '%s', '%s', '%s', '%s', '%d', '%s')" % (staff_account['id'], staff_account['username'], _mysql.escape_string(self.formdata['title']), _mysql.escape_string(message), to_name, timestamp(t), timestamp_formatted))
+
+ message = _("Added successfully.")
+ template_filename = "message.html"
+ if path_split[3] == 'delete':
+ if not administrator:
+ # We check that if he's not admin, he shouldn't be able to delete other people's posts
+ post = FetchOne("SELECT `staffid` FROM `news` WHERE id = '"+_mysql.escape_string(path_split[4])+"' AND type = '0'")
+ if post['staffid'] != staff_account['id']:
+ self.error(_('That post is not yours.'))
+ return
+
+ # Delete!
+ UpdateDb("DELETE FROM `news` WHERE id = '" + _mysql.escape_string(path_split[4]) + "' AND type = '0'")
+ message = _("Deleted successfully.")
+ template_filename = "message.html"
+ else:
+ # If he's not admin, show only his own posts
+ if administrator:
+ posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC")
+ else:
+ posts = FetchAll("SELECT * FROM `news` WHERE staffid = '"+staff_account['id']+"' AND type = '0' ORDER BY `timestamp` DESC")
+
+ template_filename = "news.html"
+ template_values = {'action': 'newschannel', 'posts': posts}
+ elif path_split[2] == 'reports':
+ if not moderator:
+ return
+
+ message = None
+ import math
+ pagesize = float(Settings.REPORTS_PER_PAGE)
+ totals = FetchOne("SELECT COUNT(id) FROM `reports`")
+ total = int(totals['COUNT(id)'])
+ pages = int(math.ceil(total / pagesize))
+
+ try:
+ currentpage = int(path_split[3])
+ except:
+ currentpage = 0
+
+ if len(path_split) > 4:
+ if path_split[4] == 'ignore':
+ # Delete report
+ UpdateDb("DELETE FROM `reports` WHERE `id` = '"+_mysql.escape_string(path_split[5])+"'")
+ message = _('Report %s ignored.') % path_split[5]
+ if 'ignore' in self.formdata.keys():
+ ignored = 0
+ if 'board' in self.formdata.keys() and self.formdata['board'] != 'all':
+ reports = FetchAll("SELECT `id` FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize))
+ else:
+ reports = FetchAll("SELECT `id` FROM `reports` ORDER BY `timestamp` DESC LIMIT %d, %d" % (currentpage*pagesize, pagesize))
+
+ for report in reports:
+ keyname = 'i' + report['id']
+ if keyname in self.formdata.keys():
+ # Ignore here
+ UpdateDb("DELETE FROM `reports` WHERE `id` = '"+_mysql.escape_string(report['id'])+"'")
+ ignored += 1
+
+ message = _('Ignored %s report(s).') % str(ignored)
+
+ # Generate board list
+ boards = FetchAll('SELECT `name`, `dir` FROM `boards` ORDER BY `dir`')
+ for board in boards:
+ if 'board' in self.formdata.keys() and self.formdata['board'] == board['dir']:
+ board['checked'] = True
+ else:
+ board['checked'] = False
+
+ # Tabla
+ if 'board' in self.formdata.keys() and self.formdata['board'] != 'all':
+ reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET_NTOA(ip) AS ip, reason, reporterip FROM `reports` WHERE `board` = '%s' ORDER BY `timestamp` DESC LIMIT %d, %d" % (_mysql.escape_string(self.formdata['board']), currentpage*pagesize, pagesize))
+ else:
+ reports = FetchAll("SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET_NTOA(ip) AS ip, reason, reporterip FROM `reports` ORDER BY `timestamp` DESC LIMIT %d, %d" % (currentpage*pagesize, pagesize))
+
+ if 'board' in self.formdata.keys():
+ curboard = self.formdata['board']
+ else:
+ curboard = None
+
+ #for report in reports:
+ # if report['parentid'] == '0':
+ # report['link'] = Settings.BOARDS_URL + report['board'] + '/res/' + report['postid'] + '.html#' + report['postid']
+ # else:
+ # report['link'] = Settings.BOARDS_URL + report['board'] + '/res/' + report['parentid'] + '.html#' + report['postid']
+
+ navigator = ''
+ if currentpage > 0:
+ navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(currentpage-1)+'">&lt;</a> '
+ else:
+ navigator += '&lt; '
+
+ for i in range(pages):
+ if i != currentpage:
+ navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(i)+'">'+str(i)+'</a> '
+ else:
+ navigator += str(i)+' '
+
+ if currentpage < (pages-1):
+ navigator += '<a href="'+Settings.CGI_URL+'manage/reports/'+str(currentpage+1)+'">&gt;</a> '
+ else:
+ navigator += '&gt; '
+
+ template_filename = "reports.html"
+ template_values = {'message': message,
+ 'boards': boards,
+ 'reports': reports,
+ 'currentpage': currentpage,
+ 'curboard': curboard,
+ 'navigator': navigator}
+ # Show by IP
+ elif path_split[2] == 'ipshow':
+ if not moderator:
+ return
+
+ if 'ip' in self.formdata.keys():
+ # If an IP was given...
+ if self.formdata['ip'] != '':
+ formatted_ip = str(inet_aton(self.formdata['ip']))
+ posts = FetchAll("SELECT posts.*, boards.dir, boards.board_type, boards.subject AS default_subject FROM `posts` JOIN `boards` ON boards.id = posts.boardid WHERE ip = '%s' ORDER BY posts.timestamp DESC" % _mysql.escape_string(formatted_ip))
+ ip = self.formdata['ip']
+ template_filename = "ipshow.html"
+ template_values = {"mode": 1, "ip": ip, "host": getHost(ip), "country": getCountry(ip), "tor": addressIsTor(ip), "posts": posts}
+ logAction(staff_account['username'], "ipshow on {}".format(ip))
+ else:
+ # Generate form
+ template_filename = "ipshow.html"
+ template_values = {"mode": 0}
+ elif path_split[2] == 'ipdelete':
+ if not moderator:
+ return
+
+ # Delete by IP
+ if 'ip' in self.formdata.keys():
+ # If an IP was given...
+ if self.formdata['ip'] != '':
+ where = []
+ if 'board_all' not in self.formdata.keys():
+ # If he chose boards separately, add them to a list
+ boards = FetchAll('SELECT `id`, `dir` FROM `boards`')
+ for board in boards:
+ keyname = 'board_' + board['dir']
+ if keyname in self.formdata.keys():
+ if self.formdata[keyname] == "1":
+ where.append(board)
+ else:
+ # If all boards were selected="selected", all them all to the list
+ where = FetchAll('SELECT `id`, `dir` FROM `boards`')
+
+ # If no board was chosen
+ if len(where) <= 0:
+ self.error(_("Select a board first."))
+ return
+
+ deletedPostsTotal = 0
+ ip = inet_aton(self.formdata['ip'])
+ deletedPosts = 0
+ for theboard in where:
+ board = setBoard(theboard['dir'])
+ isDeletedOP = False
+
+ # delete all starting posts first
+ op_posts = FetchAll("SELECT `id`, `message` FROM posts WHERE parentid = 0 AND boardid = '" + board['id'] + "' AND ip = " + str(ip))
+ for post in op_posts:
+ deletePost(post['id'], None)
+
+ deletedPosts += 1
+ deletedPostsTotal += 1
+
+ replies = FetchAll("SELECT `id`, `message`, `parentid` FROM posts WHERE parentid != 0 AND boardid = '" + board['id'] + "' AND ip = " + str(ip))
+ for post in replies:
+ deletePost(post['id'], None, '2')
+
+ deletedPosts += 1
+ deletedPostsTotal += 1
+
+ regenerateHome()
+
+ if deletedPosts > 0:
+ message = '%(posts)s post(s) were deleted from %(board)s.' % {'posts': str(deletedPosts), 'board': '/' + board['dir'] + '/'}
+ template_filename = "message.html"
+ #logAction(staff_account['username'], '%(posts)s post(s) were deleted from %(board)s. IP: %(ip)s' % \
+ # {'posts': str(deletedPosts),
+ # 'board': '/' + board['dir'] + '/',
+ # 'ip': self.formdata['ip']})
+ else:
+ self.error(_("Please enter an IP first."))
+ return
+
+ message = 'In total %(posts)s from IP %(ip)s were deleted.' % {'posts': str(deletedPosts), 'ip': self.formdata['ip']}
+ template_filename = "message.html"
+ else:
+ # Generate form...
+ template_filename = "ipdelete.html"
+ template_values = {'boards': boardlist()}
+ elif path_split[2] == 'search':
+ if not administrator:
+ return
+ search_logs = FetchAll('SELECT `id`,`timestamp`,`keyword`,`ita`,INET_NTOA(`ip`) AS `ip`,`res` FROM `search_log` ORDER BY `timestamp` DESC LIMIT 250')
+ for log in search_logs:
+ #log['ip'] = str(inet_ntoa(log['ip']))
+ log['timestamp_formatted'] = formatTimestamp(log['timestamp'])
+ if log['keyword'].startswith('k '):
+ log['keyword'] = log['keyword'][2:]
+ log['archive'] = True
+ else:
+ log['archive'] = False
+ template_filename = "search.html"
+ template_values = {'search': search_logs}
+ else:
+ # Main page.
+ reports = FetchOne("SELECT COUNT(1) FROM `reports`", 0)[0]
+ posts = FetchAll("SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC")
+
+ template_filename = "manage.html"
+ template_values = {'reports': reports, 'posts': posts}
+
+ if not skiptemplate:
+ try:
+ if template_filename == 'message.html':
+ template_values = {'message': message}
+ except:
+ template_filename = 'message.html'
+ template_values = {'message': '???'}
+
+ template_values.update({
+ 'title': 'Manage',
+ 'validated': validated,
+ 'page': page,
+ })
+
+ if validated:
+ template_values.update({
+ 'username': staff_account['username'],
+ 'site_title': Settings.SITE_TITLE,
+ 'rights': staff_account['rights'],
+ 'administrator': administrator,
+ 'added': formatTimestamp(staff_account['added']),
+ })
+
+ self.output += renderTemplate("manage/" + template_filename, template_values)
+
+def logAction(staff, action):
+ InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + ", '" + _mysql.escape_string(staff) + "\', \'" + _mysql.escape_string(action) + "\')")
+
+def genPasswd(string):
+ return getMD5(string + Settings.SECRET)
+
+def boardlist():
+ boards = FetchAll('SELECT * FROM `boards` ORDER BY `board_type`, `dir`')
+ return boards
+
+def filetypelist():
+ filetypes = FetchAll('SELECT * FROM `filetypes` ORDER BY `ext` ASC')
+ return filetypes
diff --git a/cgi/markdown.py b/cgi/markdown.py
new file mode 100644
index 0000000..3ebfaab
--- /dev/null
+++ b/cgi/markdown.py
@@ -0,0 +1,2044 @@
+#!/usr/bin/env python
+# Copyright (c) 2007-2008 ActiveState Corp.
+# License: MIT (http://www.opensource.org/licenses/mit-license.php)
+
+r"""A fast and complete Python implementation of Markdown.
+
+[from http://daringfireball.net/projects/markdown/]
+> Markdown is a text-to-HTML filter; it translates an easy-to-read /
+> easy-to-write structured text format into HTML. Markdown's text
+> format is most similar to that of plain text email, and supports
+> features such as headers, *emphasis*, code blocks, blockquotes, and
+> links.
+>
+> Markdown's syntax is designed not as a generic markup language, but
+> specifically to serve as a front-end to (X)HTML. You can use span-level
+> HTML tags anywhere in a Markdown document, and you can use block level
+> HTML tags (like <div> and <table> as well).
+
+Module usage:
+
+ >>> import markdown2
+ >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)`
+ u'<p><em>boo!</em></p>\n'
+
+ >>> markdowner = Markdown()
+ >>> markdowner.convert("*boo!*")
+ u'<p><em>boo!</em></p>\n'
+ >>> markdowner.convert("**boom!**")
+ u'<p><strong>boom!</strong></p>\n'
+
+This implementation of Markdown implements the full "core" syntax plus a
+number of extras (e.g., code syntax coloring, footnotes) as described on
+<http://code.google.com/p/python-markdown2/wiki/Extras>.
+"""
+
+cmdln_desc = """A fast and complete Python implementation of Markdown, a
+text-to-HTML conversion tool for web writers.
+
+Supported extras (see -x|--extras option below):
+* code-friendly: Disable _ and __ for em and strong.
+* code-color: Pygments-based syntax coloring of <code> sections.
+* cuddled-lists: Allow lists to be cuddled to the preceding paragraph.
+* footnotes: Support footnotes as in use on daringfireball.net and
+ implemented in other Markdown processors (tho not in Markdown.pl v1.0.1).
+* html-classes: Takes a dict mapping html tag names (lowercase) to a
+ string to use for a "class" tag attribute. Currently only supports
+ "pre" and "code" tags. Add an issue if you require this for other tags.
+* pyshell: Treats unindented Python interactive shell sessions as <code>
+ blocks.
+* link-patterns: Auto-link given regex patterns in text (e.g. bug number
+ references, revision number references).
+* xml: Passes one-liner processing instructions and namespaced XML tags.
+"""
+
+# Dev Notes:
+# - There is already a Python markdown processor
+# (http://www.freewisdom.org/projects/python-markdown/).
+# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm
+# not yet sure if there implications with this. Compare 'pydoc sre'
+# and 'perldoc perlre'.
+
+__version_info__ = (1, 0, 1, 17) # first three nums match Markdown.pl
+__version__ = '1.0.1.17'
+__author__ = "Trent Mick"
+
+import os
+import sys
+from pprint import pprint
+import re
+import logging
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+import optparse
+from random import random, randint
+import codecs
+from urllib import quote
+
+
+
+#---- Python version compat
+
+if sys.version_info[:2] < (2,4):
+ from sets import Set as set
+ def reversed(sequence):
+ for i in sequence[::-1]:
+ yield i
+ def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
+ return unicode(s, encoding, errors)
+else:
+ def _unicode_decode(s, encoding, errors='strict'):
+ return s.decode(encoding, errors)
+
+
+#---- globals
+
+DEBUG = False
+log = logging.getLogger("markdown")
+
+DEFAULT_TAB_WIDTH = 4
+
+
+try:
+ import uuid
+except ImportError:
+ SECRET_SALT = str(randint(0, 1000000))
+else:
+ SECRET_SALT = str(uuid.uuid4())
+def _hash_ascii(s):
+ #return md5(s).hexdigest() # Markdown.pl effectively does this.
+ return 'md5-' + md5(SECRET_SALT + s).hexdigest()
+def _hash_text(s):
+ return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
+
+# Table of hash values for escaped characters:
+g_escape_table = dict([(ch, _hash_ascii(ch))
+ for ch in '\\`*_{}[]()>#+-.!'])
+
+
+
+#---- exceptions
+
+class MarkdownError(Exception):
+ pass
+
+
+
+#---- public api
+
+def markdown_path(path, encoding="utf-8",
+ html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ fp = codecs.open(path, 'r', encoding)
+ text = fp.read()
+ fp.close()
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
+ safe_mode=None, extras=None, link_patterns=None,
+ use_file_vars=False):
+ return Markdown(html4tags=html4tags, tab_width=tab_width,
+ safe_mode=safe_mode, extras=extras,
+ link_patterns=link_patterns,
+ use_file_vars=use_file_vars).convert(text)
+
+class Markdown(object):
+ # The dict of "extras" to enable in processing -- a mapping of
+ # extra name to argument for the extra. Most extras do not have an
+ # argument, in which case the value is None.
+ #
+ # This can be set via (a) subclassing and (b) the constructor
+ # "extras" argument.
+ extras = None
+
+ urls = None
+ titles = None
+ html_blocks = None
+ html_spans = None
+ html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py
+
+ # Used to track when we're inside an ordered or unordered list
+ # (see _ProcessListItems() for details):
+ list_level = 0
+
+ _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
+
+ def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
+ extras=None, link_patterns=None, use_file_vars=False):
+ if html4tags:
+ self.empty_element_suffix = ">"
+ else:
+ self.empty_element_suffix = " />"
+ self.tab_width = tab_width
+
+ # For compatibility with earlier markdown2.py and with
+ # markdown.py's safe_mode being a boolean,
+ # safe_mode == True -> "replace"
+ if safe_mode is True:
+ self.safe_mode = "replace"
+ else:
+ self.safe_mode = safe_mode
+
+ if self.extras is None:
+ self.extras = {}
+ elif not isinstance(self.extras, dict):
+ self.extras = dict([(e, None) for e in self.extras])
+ if extras:
+ if not isinstance(extras, dict):
+ extras = dict([(e, None) for e in extras])
+ self.extras.update(extras)
+ assert isinstance(self.extras, dict)
+ if "toc" in self.extras and not "header-ids" in self.extras:
+ self.extras["header-ids"] = None # "toc" implies "header-ids"
+ self._instance_extras = self.extras.copy()
+ self.link_patterns = link_patterns
+ self.use_file_vars = use_file_vars
+ self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
+
+ def reset(self):
+ self.urls = {}
+ self.titles = {}
+ self.html_blocks = {}
+ self.html_spans = {}
+ self.list_level = 0
+ self.extras = self._instance_extras.copy()
+ if "footnotes" in self.extras:
+ self.footnotes = {}
+ self.footnote_ids = []
+ if "header-ids" in self.extras:
+ self._count_from_header_id = {} # no `defaultdict` in Python 2.4
+
+ def convert(self, text):
+ """Convert the given text."""
+ # Main function. The order in which other subs are called here is
+ # essential. Link and image substitutions need to happen before
+ # _EscapeSpecialChars(), so that any *'s or _'s in the <a>
+ # and <img> tags get encoded.
+
+ # Clear the global hashes. If we don't clear these, you get conflicts
+ # from other articles when generating a page which contains more than
+ # one article (e.g. an index page that shows the N most recent
+ # articles):
+ self.reset()
+
+ if not isinstance(text, unicode):
+ #TODO: perhaps shouldn't presume UTF-8 for string input?
+ text = unicode(text, 'utf-8')
+
+ if self.use_file_vars:
+ # Look for emacs-style file variable hints.
+ emacs_vars = self._get_emacs_vars(text)
+ if "markdown-extras" in emacs_vars:
+ splitter = re.compile("[ ,]+")
+ for e in splitter.split(emacs_vars["markdown-extras"]):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ self.extras[ename] = earg
+
+ # Standardize line endings:
+ text = re.sub("\r\n|\r", "\n", text)
+
+ # Make sure $text ends with a couple of newlines:
+ text += "\n\n"
+
+ # Convert all tabs to spaces.
+ text = self._detab(text)
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ \t]*\n+/ .
+ text = self._ws_only_line_re.sub("", text)
+
+ if self.safe_mode:
+ text = self._hash_html_spans(text)
+
+ # Turn block-level HTML blocks into hash entries
+ text = self._hash_html_blocks(text, raw=True)
+
+ # Strip link definitions, store in hashes.
+ if "footnotes" in self.extras:
+ # Must do footnotes first because an unlucky footnote defn
+ # looks like a link defn:
+ # [^4]: this "looks like a link defn"
+ text = self._strip_footnote_definitions(text)
+ text = self._strip_link_definitions(text)
+
+ text = self._run_block_gamut(text)
+
+ if "footnotes" in self.extras:
+ text = self._add_footnotes(text)
+
+ text = self._unescape_special_chars(text)
+
+ if self.safe_mode:
+ text = self._unhash_html_spans(text)
+
+ #text += "\n"
+
+ rv = UnicodeWithAttrs(text)
+ if "toc" in self.extras:
+ rv._toc = self._toc
+ return rv
+
+ _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE)
+ # This regular expression is intended to match blocks like this:
+ # PREFIX Local Variables: SUFFIX
+ # PREFIX mode: Tcl SUFFIX
+ # PREFIX End: SUFFIX
+ # Some notes:
+ # - "[ \t]" is used instead of "\s" to specifically exclude newlines
+ # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does
+ # not like anything other than Unix-style line terminators.
+ _emacs_local_vars_pat = re.compile(r"""^
+ (?P<prefix>(?:[^\r\n|\n|\r])*?)
+ [\ \t]*Local\ Variables:[\ \t]*
+ (?P<suffix>.*?)(?:\r\n|\n|\r)
+ (?P<content>.*?\1End:)
+ """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+ def _get_emacs_vars(self, text):
+ """Return a dictionary of emacs-style local variables.
+
+ Parsing is done loosely according to this spec (and according to
+ some in-practice deviations from this):
+ http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables
+ """
+ emacs_vars = {}
+ SIZE = pow(2, 13) # 8kB
+
+ # Search near the start for a '-*-'-style one-liner of variables.
+ head = text[:SIZE]
+ if "-*-" in head:
+ match = self._emacs_oneliner_vars_pat.search(head)
+ if match:
+ emacs_vars_str = match.group(1)
+ assert '\n' not in emacs_vars_str
+ emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';')
+ if s.strip()]
+ if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]:
+ # While not in the spec, this form is allowed by emacs:
+ # -*- Tcl -*-
+ # where the implied "variable" is "mode". This form
+ # is only allowed if there are no other variables.
+ emacs_vars["mode"] = emacs_var_strs[0].strip()
+ else:
+ for emacs_var_str in emacs_var_strs:
+ try:
+ variable, value = emacs_var_str.strip().split(':', 1)
+ except ValueError:
+ log.debug("emacs variables error: malformed -*- "
+ "line: %r", emacs_var_str)
+ continue
+ # Lowercase the variable name because Emacs allows "Mode"
+ # or "mode" or "MoDe", etc.
+ emacs_vars[variable.lower()] = value.strip()
+
+ tail = text[-SIZE:]
+ if "Local Variables" in tail:
+ match = self._emacs_local_vars_pat.search(tail)
+ if match:
+ prefix = match.group("prefix")
+ suffix = match.group("suffix")
+ lines = match.group("content").splitlines(0)
+ #print "prefix=%r, suffix=%r, content=%r, lines: %s"\
+ # % (prefix, suffix, match.group("content"), lines)
+
+ # Validate the Local Variables block: proper prefix and suffix
+ # usage.
+ for i, line in enumerate(lines):
+ if not line.startswith(prefix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper prefix '%s'"
+ % (line, prefix))
+ return {}
+ # Don't validate suffix on last line. Emacs doesn't care,
+ # neither should we.
+ if i != len(lines)-1 and not line.endswith(suffix):
+ log.debug("emacs variables error: line '%s' "
+ "does not use proper suffix '%s'"
+ % (line, suffix))
+ return {}
+
+ # Parse out one emacs var per line.
+ continued_for = None
+ for line in lines[:-1]: # no var on the last line ("PREFIX End:")
+ if prefix: line = line[len(prefix):] # strip prefix
+ if suffix: line = line[:-len(suffix)] # strip suffix
+ line = line.strip()
+ if continued_for:
+ variable = continued_for
+ if line.endswith('\\'):
+ line = line[:-1].rstrip()
+ else:
+ continued_for = None
+ emacs_vars[variable] += ' ' + line
+ else:
+ try:
+ variable, value = line.split(':', 1)
+ except ValueError:
+ log.debug("local variables error: missing colon "
+ "in local variables entry: '%s'" % line)
+ continue
+ # Do NOT lowercase the variable name, because Emacs only
+ # allows "mode" (and not "Mode", "MoDe", etc.) in this block.
+ value = value.strip()
+ if value.endswith('\\'):
+ value = value[:-1].rstrip()
+ continued_for = variable
+ else:
+ continued_for = None
+ emacs_vars[variable] = value
+
+ # Unquote values.
+ for var, val in emacs_vars.items():
+ if len(val) > 1 and (val.startswith('"') and val.endswith('"')
+ or val.startswith('"') and val.endswith('"')):
+ emacs_vars[var] = val[1:-1]
+
+ return emacs_vars
+
+ # Cribbed from a post by Bart Lateur:
+ # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+ _detab_re = re.compile(r'(.*?)\t', re.M)
+ def _detab_sub(self, match):
+ g1 = match.group(1)
+ return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
+ def _detab(self, text):
+ r"""Remove (leading?) tabs from a file.
+
+ >>> m = Markdown()
+ >>> m._detab("\tfoo")
+ ' foo'
+ >>> m._detab(" \tfoo")
+ ' foo'
+ >>> m._detab("\t foo")
+ ' foo'
+ >>> m._detab(" foo")
+ ' foo'
+ >>> m._detab(" foo\n\tbar\tblam")
+ ' foo\n bar blam'
+ """
+ if '\t' not in text:
+ return text
+ return self._detab_re.subn(self._detab_sub, text)[0]
+
+ _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'
+ _strict_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ </\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_a,
+ re.X | re.M)
+
+ _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'
+ _liberal_tag_block_re = re.compile(r"""
+ ( # save in \1
+ ^ # start of line (with re.M)
+ <(%s) # start tag = \2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ .*</\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ """ % _block_tags_b,
+ re.X | re.M)
+
+ def _hash_html_block_sub(self, match, raw=False):
+ html = match.group(1)
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ return "\n\n" + key + "\n\n"
+
+ def _hash_html_blocks(self, text, raw=False):
+ """Hashify HTML blocks
+
+ We only want to do this for block-level HTML tags, such as headers,
+ lists, and tables. That's because we still want to wrap <p>s around
+ "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ phrase emphasis, and spans. The list of tags we're looking for is
+ hard-coded.
+
+ @param raw {boolean} indicates if these are raw HTML blocks in
+ the original source. It makes a difference in "safe" mode.
+ """
+ if '<' not in text:
+ return text
+
+ # Pass `raw` value into our calls to self._hash_html_block_sub.
+ hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ text = self._strict_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
+ text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)
+
+ # Special case just for <hr />. It was easier to make a special
+ # case than to make the other regex more complicated.
+ if "<hr" in text:
+ _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)
+ text = _hr_tag_re.sub(hash_html_block_sub, text)
+
+ # Special case for standalone HTML comments:
+ if "<!--" in text:
+ start = 0
+ while True:
+ # Delimiters for next comment block.
+ try:
+ start_idx = text.index("<!--", start)
+ except ValueError, ex:
+ break
+ try:
+ end_idx = text.index("-->", start_idx) + 3
+ except ValueError, ex:
+ break
+
+ # Start position for next comment block search.
+ start = end_idx
+
+ # Validate whitespace before comment.
+ if start_idx:
+ # - Up to `tab_width - 1` spaces before start_idx.
+ for i in range(self.tab_width - 1):
+ if text[start_idx - 1] != ' ':
+ break
+ start_idx -= 1
+ if start_idx == 0:
+ break
+ # - Must be preceded by 2 newlines or hit the start of
+ # the document.
+ if start_idx == 0:
+ pass
+ elif start_idx == 1 and text[0] == '\n':
+ start_idx = 0 # to match minute detail of Markdown.pl regex
+ elif text[start_idx-2:start_idx] == '\n\n':
+ pass
+ else:
+ break
+
+ # Validate whitespace after comment.
+ # - Any number of spaces and tabs.
+ while end_idx < len(text):
+ if text[end_idx] not in ' \t':
+ break
+ end_idx += 1
+ # - Must be following by 2 newlines or hit end of text.
+ if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):
+ continue
+
+ # Escape and hash (must match `_hash_html_block_sub`).
+ html = text[start_idx:end_idx]
+ if raw and self.safe_mode:
+ html = self._sanitize_html(html)
+ key = _hash_text(html)
+ self.html_blocks[key] = html
+ text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]
+
+ if "xml" in self.extras:
+ # Treat XML processing instructions and namespaced one-liner
+ # tags as if they were block HTML tags. E.g., if standalone
+ # (i.e. are their own paragraph), the following do not get
+ # wrapped in a <p> tag:
+ # <?foo bar?>
+ #
+ # <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>
+ _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)
+ text = _xml_oneliner_re.sub(hash_html_block_sub, text)
+
+ return text
+
+ def _strip_link_definitions(self, text):
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ less_than_tab = self.tab_width - 1
+
+ # Link defs are in the form:
+ # [id]: url "optional title"
+ _link_def_re = re.compile(r"""
+ ^[ ]{0,%d}\[(.+)\]: # id = \1
+ [ \t]*
+ \n? # maybe *one* newline
+ [ \t]*
+ <?(.+?)>? # url = \2
+ [ \t]*
+ (?:
+ \n? # maybe one newline
+ [ \t]*
+ (?<=\s) # lookbehind for whitespace
+ ['"(]
+ ([^\n]*) # title = \3
+ ['")]
+ [ \t]*
+ )? # title is optional
+ (?:\n+|\Z)
+ """ % less_than_tab, re.X | re.M | re.U)
+ return _link_def_re.sub(self._extract_link_def_sub, text)
+
+ def _extract_link_def_sub(self, match):
+ id, url, title = match.groups()
+ key = id.lower() # Link IDs are case-insensitive
+ self.urls[key] = self._encode_amps_and_angles(url)
+ if title:
+ self.titles[key] = title.replace('"', '&quot;')
+ return ""
+
+ def _extract_footnote_def_sub(self, match):
+ id, text = match.groups()
+ text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()
+ normed_id = re.sub(r'\W', '-', id)
+ # Ensure footnote text ends with a couple newlines (for some
+ # block gamut matches).
+ self.footnotes[normed_id] = text + "\n\n"
+ return ""
+
+ def _strip_footnote_definitions(self, text):
+ """A footnote definition looks like this:
+
+ [^note-id]: Text of the note.
+
+ May include one or more indented paragraphs.
+
+ Where,
+ - The 'note-id' can be pretty much anything, though typically it
+ is the number of the footnote.
+ - The first paragraph may start on the next line, like so:
+
+ [^note-id]:
+ Text of the note.
+ """
+ less_than_tab = self.tab_width - 1
+ footnote_def_re = re.compile(r'''
+ ^[ ]{0,%d}\[\^(.+)\]: # id = \1
+ [ \t]*
+ ( # footnote text = \2
+ # First line need not start with the spaces.
+ (?:\s*.*\n+)
+ (?:
+ (?:[ ]{%d} | \t) # Subsequent lines must be indented.
+ .*\n+
+ )*
+ )
+ # Lookahead for non-space at line-start, or end of doc.
+ (?:(?=^[ ]{0,%d}\S)|\Z)
+ ''' % (less_than_tab, self.tab_width, self.tab_width),
+ re.X | re.M)
+ return footnote_def_re.sub(self._extract_footnote_def_sub, text)
+
+
+ _hr_res = [
+ re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M),
+ re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M),
+ ]
+
+ def _run_block_gamut(self, text):
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+
+ #text = self._do_headers(text)
+
+ # Do Horizontal Rules:
+ #hr = "\n<hr"+self.empty_element_suffix+"\n"
+ #for hr_re in self._hr_res:
+ # text = hr_re.sub(hr, text)
+
+ text = self._do_lists(text)
+
+ if "pyshell" in self.extras:
+ text = self._prepare_pyshell_blocks(text)
+
+ text = self._do_code_blocks(text)
+
+ text = self._do_block_quotes(text)
+
+ # We already ran _HashHTMLBlocks() before, in Markdown(), but that
+ # was to escape raw HTML in the original Markdown source. This time,
+ # we're escaping the markup we've just created, so that we don't wrap
+ # <p> tags around block-level tags.
+ text = self._hash_html_blocks(text)
+
+ text = self._form_paragraphs(text)
+
+ return text
+
+ def _pyshell_block_sub(self, match):
+ lines = match.group(0).splitlines(0)
+ _dedentlines(lines)
+ indent = ' ' * self.tab_width
+ s = ('\n' # separate from possible cuddled paragraph
+ + indent + ('\n'+indent).join(lines)
+ + '\n\n')
+ return s
+
+ def _prepare_pyshell_blocks(self, text):
+ """Ensure that Python interactive shell sessions are put in
+ code blocks -- even if not properly indented.
+ """
+ if ">>>" not in text:
+ return text
+
+ less_than_tab = self.tab_width - 1
+ _pyshell_block_re = re.compile(r"""
+ ^([ ]{0,%d})>>>[ ].*\n # first line
+ ^(\1.*\S+.*\n)* # any number of subsequent lines
+ ^\n # ends with a blank line
+ """ % less_than_tab, re.M | re.X)
+
+ return _pyshell_block_re.sub(self._pyshell_block_sub, text)
+
+ def _run_span_gamut(self, text):
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+
+ #text = self._do_code_spans(text) - El AA !
+
+ text = self._escape_special_chars(text)
+
+ # Process anchor and image tags.
+ text = self._do_links(text)
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after _do_links(), because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ #text = self._do_auto_links(text)
+
+ if "link-patterns" in self.extras:
+ text = self._do_link_patterns(text)
+
+ text = self._encode_amps_and_angles(text)
+
+ text = self._do_italics_and_bold(text)
+
+ # Do hard breaks:
+ text = re.sub(r"\n", "<br%s" % self.empty_element_suffix, text)
+
+ return text
+
+ # "Sorta" because auto-links are identified as "tag" tokens.
+ _sorta_html_tokenize_re = re.compile(r"""
+ (
+ # tag
+ </?
+ (?:\w+) # tag name
+ (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))* # attributes
+ \s*/?>
+ |
+ # auto-link (e.g., <http://www.activestate.com/>)
+ <\w+[^>]*>
+ |
+ <!--.*?--> # comment
+ |
+ <\?.*?\?> # processing instruction
+ )
+ """, re.X)
+
+ def _escape_special_chars(self, text):
+ # Python markdown note: the HTML tokenization here differs from
+ # that in Markdown.pl, hence the behaviour for subtle cases can
+ # differ (I believe the tokenizer here does a better job because
+ # it isn't susceptible to unmatched '<' and '>' in HTML tags).
+ # Note, however, that '>' is not allowed in an auto-link URL
+ # here.
+ escaped = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup:
+ # Within tags/HTML-comments/auto-links, encode * and _
+ # so they don't conflict with their use in Markdown for
+ # italics and strong. We're replacing each such
+ # character with its corresponding MD5 checksum value;
+ # this is likely overkill, but it should prevent us from
+ # colliding with the escape values by accident.
+ escaped.append(token.replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ else:
+ escaped.append(self._encode_backslash_escapes(token))
+ is_html_markup = not is_html_markup
+ return ''.join(escaped)
+
+ def _hash_html_spans(self, text):
+ # Used for safe_mode.
+
+ def _is_auto_link(s):
+ if ':' in s and self._auto_link_re.match(s):
+ return True
+ elif '@' in s and self._auto_email_link_re.match(s):
+ return True
+ return False
+
+ tokens = []
+ is_html_markup = False
+ for token in self._sorta_html_tokenize_re.split(text):
+ if is_html_markup and not _is_auto_link(token):
+ sanitized = self._sanitize_html(token)
+ key = _hash_text(sanitized)
+ self.html_spans[key] = sanitized
+ tokens.append(key)
+ else:
+ tokens.append(token)
+ is_html_markup = not is_html_markup
+ return ''.join(tokens)
+
+ def _unhash_html_spans(self, text):
+ for key, sanitized in self.html_spans.items():
+ text = text.replace(key, sanitized)
+ return text
+
+ def _sanitize_html(self, s):
+ if self.safe_mode == "replace":
+ return self.html_removed_text
+ elif self.safe_mode == "escape":
+ replacements = [
+ ('&', '&amp;'),
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ ]
+ for before, after in replacements:
+ s = s.replace(before, after)
+ return s
+ else:
+ raise MarkdownError("invalid value for 'safe_mode': %r (must be "
+ "'escape' or 'replace')" % self.safe_mode)
+
+ _tail_of_inline_link_re = re.compile(r'''
+ # Match tail of: [text](/url/) or [text](/url/ "title")
+ \( # literal paren
+ [ \t]*
+ (?P<url> # \1
+ <.*?>
+ |
+ .*?
+ )
+ [ \t]*
+ ( # \2
+ (['"]) # quote char = \3
+ (?P<title>.*?)
+ \3 # matching quote
+ )? # title is optional
+ \)
+ ''', re.X | re.S)
+ _tail_of_reference_link_re = re.compile(r'''
+ # Match tail of: [text][id]
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+ \[
+ (?P<id>.*?)
+ \]
+ ''', re.X | re.S)
+
+ def _do_links(self, text):
+ """Turn Markdown link shortcuts into XHTML <a> and <img> tags.
+
+ This is a combination of Markdown.pl's _DoAnchors() and
+ _DoImages(). They are done together because that simplified the
+ approach. It was necessary to use a different approach than
+ Markdown.pl because of the lack of atomic matching support in
+ Python's regex engine used in $g_nested_brackets.
+ """
+ MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24
+
+ # `anchor_allowed_pos` is used to support img links inside
+ # anchors, but not anchors inside anchors. An anchor's start
+ # pos must be `>= anchor_allowed_pos`.
+ anchor_allowed_pos = 0
+
+ curr_pos = 0
+ while True: # Handle the next link.
+ # The next '[' is the start of:
+ # - an inline anchor: [text](url "title")
+ # - a reference anchor: [text][id]
+ # - an inline img: ![text](url "title")
+ # - a reference img: ![text][id]
+ # - a footnote ref: [^id]
+ # (Only if 'footnotes' extra enabled)
+ # - a footnote defn: [^id]: ...
+ # (Only if 'footnotes' extra enabled) These have already
+ # been stripped in _strip_footnote_definitions() so no
+ # need to watch for them.
+ # - a link definition: [id]: url "title"
+ # These have already been stripped in
+ # _strip_link_definitions() so no need to watch for them.
+ # - not markup: [...anything else...
+ try:
+ start_idx = text.index('[', curr_pos)
+ except ValueError:
+ break
+ text_length = len(text)
+
+ # Find the matching closing ']'.
+ # Markdown.pl allows *matching* brackets in link text so we
+ # will here too. Markdown.pl *doesn't* currently allow
+ # matching brackets in img alt text -- we'll differ in that
+ # regard.
+ bracket_depth = 0
+ for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,
+ text_length)):
+ ch = text[p]
+ if ch == ']':
+ bracket_depth -= 1
+ if bracket_depth < 0:
+ break
+ elif ch == '[':
+ bracket_depth += 1
+ else:
+ # Closing bracket not found within sentinel length.
+ # This isn't markup.
+ curr_pos = start_idx + 1
+ continue
+ link_text = text[start_idx+1:p]
+
+ # Possibly a footnote ref?
+ if "footnotes" in self.extras and link_text.startswith("^"):
+ normed_id = re.sub(r'\W', '-', link_text[1:])
+ if normed_id in self.footnotes:
+ self.footnote_ids.append(normed_id)
+ result = '<sup class="footnote-ref" id="fnref-%s">' \
+ '<a href="#fn-%s">%s</a></sup>' \
+ % (normed_id, normed_id, len(self.footnote_ids))
+ text = text[:start_idx] + result + text[p+1:]
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = p+1
+ continue
+
+ # Now determine what this is by the remainder.
+ p += 1
+ if p == text_length:
+ return text
+
+ # Inline anchor or img?
+ if text[p] == '(': # attempt at perf improvement
+ match = self._tail_of_inline_link_re.match(text, p)
+ if match:
+ # Handle an inline anchor or img.
+ #is_img = start_idx > 0 and text[start_idx-1] == "!"
+ #if is_img:
+ # start_idx -= 1
+ is_img = False
+
+ url, title = match.group("url"), match.group("title")
+ if url and url[0] == '<':
+ url = url[1:-1] # '<url>' -> 'url'
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ if title:
+ title_str = ' title="%s"' \
+ % title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_']) \
+ .replace('"', '&quot;')
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url.replace('"', '&quot;'),
+ link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ continue
+
+ # Reference anchor or img?
+ else:
+ match = self._tail_of_reference_link_re.match(text, p)
+ if match:
+ # Handle a reference-style anchor or img.
+ #is_img = start_idx > 0 and text[start_idx-1] == "!"
+ #if is_img:
+ # start_idx -= 1
+ is_img = False
+
+ link_id = match.group("id").lower()
+ if not link_id:
+ link_id = link_text.lower() # for links like [this][]
+ if link_id in self.urls:
+ url = self.urls[link_id]
+ # We've got to encode these to avoid conflicting
+ # with italics/bold.
+ url = url.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title = self.titles.get(link_id)
+ if title:
+ title = title.replace('*', g_escape_table['*']) \
+ .replace('_', g_escape_table['_'])
+ title_str = ' title="%s"' % title
+ else:
+ title_str = ''
+ if is_img:
+ result = '<img src="%s" alt="%s"%s%s' \
+ % (url.replace('"', '&quot;'),
+ link_text.replace('"', '&quot;'),
+ title_str, self.empty_element_suffix)
+ curr_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ elif start_idx >= anchor_allowed_pos:
+ result = '<a href="%s"%s>%s</a>' \
+ % (url, title_str, link_text)
+ result_head = '<a href="%s"%s>' % (url, title_str)
+ result = '%s%s</a>' % (result_head, link_text)
+ # <img> allowed from curr_pos on, <a> from
+ # anchor_allowed_pos on.
+ curr_pos = start_idx + len(result_head)
+ anchor_allowed_pos = start_idx + len(result)
+ text = text[:start_idx] + result + text[match.end():]
+ else:
+ # Anchor not allowed here.
+ curr_pos = start_idx + 1
+ else:
+ # This id isn't defined, leave the markup alone.
+ curr_pos = match.end()
+ continue
+
+ # Otherwise, it isn't markup.
+ curr_pos = start_idx + 1
+
+ return text
+
+ def header_id_from_text(self, text, prefix):
+ """Generate a header id attribute value from the given header
+ HTML content.
+
+ This is only called if the "header-ids" extra is enabled.
+ Subclasses may override this for different header ids.
+ """
+ header_id = _slugify(text)
+ if prefix:
+ header_id = prefix + '-' + header_id
+ if header_id in self._count_from_header_id:
+ self._count_from_header_id[header_id] += 1
+ header_id += '-%s' % self._count_from_header_id[header_id]
+ else:
+ self._count_from_header_id[header_id] = 1
+ return header_id
+
+ _toc = None
+ def _toc_add_entry(self, level, id, name):
+ if self._toc is None:
+ self._toc = []
+ self._toc.append((level, id, name))
+
+ _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)
+ def _setext_h_sub(self, match):
+ n = {"=": 1, "-": 2}[match.group(2)[0]]
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ header_id_attr = ""
+ if "header-ids" in self.extras:
+ header_id = self.header_id_from_text(match.group(1),
+ prefix=self.extras["header-ids"])
+ header_id_attr = ' id="%s"' % header_id
+ html = self._run_span_gamut(match.group(1))
+ if "toc" in self.extras:
+ self._toc_add_entry(n, header_id, html)
+ return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n)
+
+ _atx_h_re = re.compile(r'''
+ ^(\#{1,6}) # \1 = string of #'s
+ [ \t]*
+ (.+?) # \2 = Header text
+ [ \t]*
+ (?<!\\) # ensure not an escaped trailing '#'
+ \#* # optional closing #'s (not counted)
+ \n+
+ ''', re.X | re.M)
+ def _atx_h_sub(self, match):
+ n = len(match.group(1))
+ demote_headers = self.extras.get("demote-headers")
+ if demote_headers:
+ n = min(n + demote_headers, 6)
+ header_id_attr = ""
+ if "header-ids" in self.extras:
+ header_id = self.header_id_from_text(match.group(2),
+ prefix=self.extras["header-ids"])
+ header_id_attr = ' id="%s"' % header_id
+ html = self._run_span_gamut(match.group(2))
+ if "toc" in self.extras:
+ self._toc_add_entry(n, header_id, html)
+ return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n)
+
+ def _do_headers(self, text):
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ text = self._setext_h_re.sub(self._setext_h_sub, text)
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ text = self._atx_h_re.sub(self._atx_h_sub, text)
+
+ return text
+
+
+ _marker_ul_chars = '*+-'
+ _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars
+ _marker_ul = '(?:[%s])' % _marker_ul_chars
+ _marker_ol = r'(?:\d+\.)'
+
+ def _list_sub(self, match):
+ lst = match.group(1)
+ lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"
+ result = self._process_list_items(lst)
+ if self.list_level:
+ return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)
+ else:
+ return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)
+
+ def _do_lists(self, text):
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+
+ for marker_pat in (self._marker_ul, self._marker_ol):
+ # Re-usable pattern to match any entire ul or ol list:
+ less_than_tab = self.tab_width - 1
+ whole_list = r'''
+ ( # \1 = whole list
+ ( # \2
+ [ ]{0,%d}
+ (%s) # \3 = first list item marker
+ [ \t]+
+ )
+ (?:.+?)
+ ( # \4
+ \Z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ \t]*
+ %s[ \t]+
+ )
+ )
+ )
+ ''' % (less_than_tab, marker_pat, marker_pat)
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _process_list_items().
+ #
+ # Note: There's a bit of duplication here. My original implementation
+ # created a scalar regex pattern as the conditional result of the test on
+ # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
+ # substitution once, using the scalar as the pattern. This worked,
+ # everywhere except when running under MT on my hosting account at Pair
+ # Networks. There, this caused all rebuilds to be killed by the reaper (or
+ # perhaps they crashed, but that seems incredibly unlikely given that the
+ # same script on the same server ran fine *except* under MT. I've spent
+ # more time trying to figure out why this is happening than I'd like to
+ # admit. My only guess, backed up by the fact that this workaround works,
+ # is that Perl optimizes the substition when it can figure out that the
+ # pattern will never change, and when this optimization isn't on, we run
+ # afoul of the reaper. Thus, the slightly redundant code to that uses two
+ # static s/// patterns rather than one conditional pattern.
+
+ if self.list_level:
+ sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S)
+ text = sub_list_re.sub(self._list_sub, text)
+ else:
+ list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,
+ re.X | re.M | re.S)
+ text = list_re.sub(self._list_sub, text)
+
+ return text
+
+ _list_item_re = re.compile(r'''
+ (\n)? # leading line = \1
+ (^[ \t]*) # leading whitespace = \2
+ (?P<marker>%s) [ \t]+ # list marker = \3
+ ((?:.+?) # list item text = \4
+ (\n{1,2})) # eols = \5
+ (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+))
+ ''' % (_marker_any, _marker_any),
+ re.M | re.X | re.S)
+
+ _last_li_endswith_two_eols = False
+ def _list_item_sub(self, match):
+ item = match.group(4)
+ leading_line = match.group(1)
+ leading_space = match.group(2)
+ if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:
+ item = self._run_block_gamut(self._outdent(item))
+ else:
+ # Recursion for sub-lists:
+ item = self._do_lists(self._outdent(item))
+ if item.endswith('\n'):
+ item = item[:-1]
+ item = self._run_span_gamut(item)
+ self._last_li_endswith_two_eols = (len(match.group(5)) == 2)
+ return "<li>%s</li>\n" % item
+
+ def _process_list_items(self, list_str):
+ # Process the contents of a single ordered or unordered list,
+ # splitting it into individual list items.
+
+ # The $g_list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+ self.list_level += 1
+ self._last_li_endswith_two_eols = False
+ list_str = list_str.rstrip('\n') + '\n'
+ list_str = self._list_item_re.sub(self._list_item_sub, list_str)
+ self.list_level -= 1
+ return list_str
+
+ def _get_pygments_lexer(self, lexer_name):
+ try:
+ from pygments import lexers, util
+ except ImportError:
+ return None
+ try:
+ return lexers.get_lexer_by_name(lexer_name)
+ except util.ClassNotFound:
+ return None
+
+ def _color_with_pygments(self, codeblock, lexer, **formatter_opts):
+ import pygments
+ import pygments.formatters
+
+ class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
+ def _wrap_code(self, inner):
+ """A function for use in a Pygments Formatter which
+ wraps in <code> tags.
+ """
+ yield 0, "<code>"
+ for tup in inner:
+ yield tup
+ yield 0, "</code>"
+
+ def wrap(self, source, outfile):
+ """Return the source with a code, pre, and div."""
+ return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
+
+ formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts)
+ return pygments.highlight(codeblock, lexer, formatter)
+
+ def _code_block_sub(self, match):
+ codeblock = match.group(1)
+ codeblock = self._outdent(codeblock)
+ codeblock = self._detab(codeblock)
+ codeblock = codeblock.lstrip('\n') # trim leading newlines
+ codeblock = codeblock.rstrip() # trim trailing whitespace
+
+ if "code-color" in self.extras and codeblock.startswith(":::"):
+ lexer_name, rest = codeblock.split('\n', 1)
+ lexer_name = lexer_name[3:].strip()
+ lexer = self._get_pygments_lexer(lexer_name)
+ codeblock = rest.lstrip("\n") # Remove lexer declaration line.
+ if lexer:
+ formatter_opts = self.extras['code-color'] or {}
+ colored = self._color_with_pygments(codeblock, lexer,
+ **formatter_opts)
+ return "\n\n%s\n\n" % colored
+
+ codeblock = self._encode_code(codeblock)
+ pre_class_str = self._html_class_str_from_tag("pre")
+ code_class_str = self._html_class_str_from_tag("code")
+ return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % (
+ pre_class_str, code_class_str, codeblock)
+
+ def _html_class_str_from_tag(self, tag):
+ """Get the appropriate ' class="..."' string (note the leading
+ space), if any, for the given tag.
+ """
+ if "html-classes" not in self.extras:
+ return ""
+ try:
+ html_classes_from_tag = self.extras["html-classes"]
+ except TypeError:
+ return ""
+ else:
+ if tag in html_classes_from_tag:
+ return ' class="%s"' % html_classes_from_tag[tag]
+ return ""
+
+ def _do_code_blocks(self, text):
+ """Process Markdown `<pre><code>` blocks."""
+ code_block_re = re.compile(r'''
+ (?:\n\n|\A)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ ''' % (self.tab_width, self.tab_width),
+ re.M | re.X)
+
+ return code_block_re.sub(self._code_block_sub, text)
+
+
+ # Rules for a code span:
+ # - backslash escapes are not interpreted in a code span
+ # - to include one or or a run of more backticks the delimiters must
+ # be a longer run of backticks
+ # - cannot start or end a code span with a backtick; pad with a
+ # space and that space will be removed in the emitted HTML
+ # See `test/tm-cases/escapes.text` for a number of edge-case
+ # examples.
+ _code_span_re = re.compile(r'''
+ (?<!\\)
+ (`+) # \1 = Opening run of `
+ (?!`) # See Note A test/tm-cases/escapes.text
+ (.+?) # \2 = The code block
+ (?<!`)
+ \1 # Matching closer
+ (?!`)
+ ''', re.X | re.S)
+
+ def _code_span_sub(self, match):
+ c = match.group(2).strip(" \t")
+ c = self._encode_code(c)
+ return "<code>%s</code>" % c
+
+ def _do_code_spans(self, text):
+ # * Backtick quotes are used for <code></code> spans.
+ #
+ # * You can use multiple backticks as the delimiters if you want to
+ # include literal backticks in the code span. So, this input:
+ #
+ # Just type ``foo `bar` baz`` at the prompt.
+ #
+ # Will translate to:
+ #
+ # <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+ #
+ # There's no arbitrary limit to the number of backticks you
+ # can use as delimters. If you need three consecutive backticks
+ # in your code, use four for delimiters, etc.
+ #
+ # * You can use spaces to get literal backticks at the edges:
+ #
+ # ... type `` `bar` `` ...
+ #
+ # Turns to:
+ #
+ # ... type <code>`bar`</code> ...
+ return self._code_span_re.sub(self._code_span_sub, text)
+
+ def _encode_code(self, text):
+ """Encode/escape certain characters inside Markdown code runs.
+ The point is that in code, these characters are literals,
+ and lose their special Markdown meanings.
+ """
+ replacements = [
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ ('&', '&amp;'),
+ # Do the angle bracket song and dance:
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ # Now, escape characters that are magic in Markdown:
+ ('*', g_escape_table['*']),
+ ('_', g_escape_table['_']),
+ ('{', g_escape_table['{']),
+ ('}', g_escape_table['}']),
+ ('[', g_escape_table['[']),
+ (']', g_escape_table[']']),
+ ('\\', g_escape_table['\\']),
+ ]
+ for before, after in replacements:
+ text = text.replace(before, after)
+ return text
+
+ _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
+ _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
+ #_spoiler_re = re.compile(r"###(?=\S)(.+?[*_]*)(?<=\S)###", re.S)
+
+ _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
+ _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
+ def _do_italics_and_bold(self, text):
+ # <strong> must go first:
+ if "code-friendly" in self.extras:
+ text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)
+ text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)
+ else:
+ text = self._strong_re.sub(r"<strong>\2</strong>", text)
+ text = self._em_re.sub(r"<em>\2</em>", text)
+
+ #text = self._spoiler_re.sub("<del>\\1</del>", text)
+ return text
+
+
+ _block_quote_re = re.compile(r'''
+ ( # Wrap whole match in \1
+ (
+ ^[ \t]*>[^>] # '>' at the start of a line
+ .+\n # rest of the first line
+ \n* # blanks
+ )+
+ )
+ ''', re.M | re.X)
+ _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
+
+ _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
+ def _dedent_two_spaces_sub(self, match):
+ return re.sub(r'(?m)^ ', '', match.group(1))
+
+ def _block_quote_sub(self, match):
+ bq = match.group(1)
+ #bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting
+ bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines
+ bq = bq.strip('\n')
+ bq = self._run_span_gamut(bq)
+ #bq = self._run_block_gamut(bq) # recurse
+
+ bq = re.sub('(?m)^', ' ', bq)
+ # These leading spaces screw with <pre> content, so we need to fix that:
+ bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
+
+ return "<blockquote>\n%s\n</blockquote>\n\n" % bq
+
+ def _do_block_quotes(self, text):
+ if '>' not in text:
+ return text
+ return self._block_quote_re.sub(self._block_quote_sub, text)
+
+ def _form_paragraphs(self, text):
+ # Strip leading and trailing lines:
+ text = text.strip('\n')
+
+ # Wrap <p> tags.
+ grafs = []
+ for i, graf in enumerate(re.split(r"\n{2,}", text)):
+ if graf in self.html_blocks:
+ # Unhashify HTML blocks
+ grafs.append(self.html_blocks[graf])
+ else:
+ cuddled_list = None
+ if "cuddled-lists" in self.extras:
+ # Need to put back trailing '\n' for `_list_item_re`
+ # match at the end of the paragraph.
+ li = self._list_item_re.search(graf + '\n')
+ # Two of the same list marker in this paragraph: a likely
+ # candidate for a list cuddled to preceding paragraph
+ # text (issue 33). Note the `[-1]` is a quick way to
+ # consider numeric bullets (e.g. "1." and "2.") to be
+ # equal.
+ if (li and len(li.group(2)) <= 3 and li.group("next_marker")
+ and li.group("marker")[-1] == li.group("next_marker")[-1]):
+ start = li.start()
+ cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
+ assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
+ graf = graf[:start]
+
+ # Wrap <p> tags.
+ graf = self._run_span_gamut(graf)
+ grafs.append("<p>" + graf.lstrip(" \t") + "</p>")
+
+ if cuddled_list:
+ grafs.append(cuddled_list)
+
+ return "\n\n".join(grafs)
+
+ def _add_footnotes(self, text):
+ if self.footnotes:
+ footer = [
+ '<div class="footnotes">',
+ '<hr' + self.empty_element_suffix,
+ '<ol>',
+ ]
+ for i, id in enumerate(self.footnote_ids):
+ if i != 0:
+ footer.append('')
+ footer.append('<li id="fn-%s">' % id)
+ footer.append(self._run_block_gamut(self.footnotes[id]))
+ backlink = ('<a href="#fnref-%s" '
+ 'class="footnoteBackLink" '
+ 'title="Jump back to footnote %d in the text.">'
+ '&#8617;</a>' % (id, i+1))
+ if footer[-1].endswith("</p>"):
+ footer[-1] = footer[-1][:-len("</p>")] \
+ + '&nbsp;' + backlink + "</p>"
+ else:
+ footer.append("\n<p>%s</p>" % backlink)
+ footer.append('</li>')
+ footer.append('</ol>')
+ footer.append('</div>')
+ return text + '\n\n' + '\n'.join(footer)
+ else:
+ return text
+
+ # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ # http://bumppo.net/projects/amputator/
+ _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
+ _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
+ _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I)
+
+ def _encode_amps_and_angles(self, text):
+ # Smart processing for ampersands and angle brackets that need
+ # to be encoded.
+ text = self._ampersand_re.sub('&amp;', text)
+
+ # Encode naked <'s
+ text = self._naked_lt_re.sub('&lt;', text)
+
+ # Encode naked >'s
+ # Note: Other markdown implementations (e.g. Markdown.pl, PHP
+ # Markdown) don't do this.
+ text = self._naked_gt_re.sub('&gt;', text)
+ return text
+
+ def _encode_backslash_escapes(self, text):
+ for ch, escape in g_escape_table.items():
+ text = text.replace("\\"+ch, escape)
+ return text
+
+ _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
+ def _auto_link_sub(self, match):
+ g1 = match.group(1)
+ return '<a href="%s">%s</a>' % (g1, g1)
+
+ _auto_email_link_re = re.compile(r"""
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-\w]+(\.[-\w]+)*\.[a-z]+
+ )
+ >
+ """, re.I | re.X | re.U)
+ def _auto_email_link_sub(self, match):
+ return self._encode_email_address(
+ self._unescape_special_chars(match.group(1)))
+
+ def _do_auto_links(self, text):
+ text = self._auto_link_re.sub(self._auto_link_sub, text)
+ text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
+ return text
+
+ def _encode_email_address(self, addr):
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ # <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+ # x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+ # &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+ #
+ # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+ # mailing list: <http://tinyurl.com/yu7ue>
+ chars = [_xml_encode_email_char_at_random(ch)
+ for ch in "mailto:" + addr]
+ # Strip the mailto: from the visible part.
+ addr = '<a href="%s">%s</a>' \
+ % (''.join(chars), ''.join(chars[7:]))
+ return addr
+
+ def _do_link_patterns(self, text):
+ """Caveat emptor: there isn't much guarding against link
+ patterns being formed inside other standard Markdown links, e.g.
+ inside a [link def][like this].
+
+ Dev Notes: *Could* consider prefixing regexes with a negative
+ lookbehind assertion to attempt to guard against this.
+ """
+ link_from_hash = {}
+ for regex, repl in self.link_patterns:
+ replacements = []
+ for match in regex.finditer(text):
+ if hasattr(repl, "__call__"):
+ href = repl(match)
+ else:
+ href = match.expand(repl)
+ replacements.append((match.span(), href))
+ for (start, end), href in reversed(replacements):
+ escaped_href = (
+ href.replace('"', '&quot;') # b/c of attr quote
+ # To avoid markdown <em> and <strong>:
+ .replace('*', g_escape_table['*'])
+ .replace('_', g_escape_table['_']))
+ link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])
+ hash = _hash_text(link)
+ link_from_hash[hash] = link
+ text = text[:start] + hash + text[end:]
+ for hash, link in link_from_hash.items():
+ text = text.replace(hash, link)
+ return text
+
+ def _unescape_special_chars(self, text):
+ # Swap back in all the special characters we've hidden.
+ for ch, hash in g_escape_table.items():
+ text = text.replace(hash, ch)
+ return text
+
+ def _outdent(self, text):
+ # Remove one level of line-leading tabs or spaces
+ return self._outdent_re.sub('', text)
+
+
+class MarkdownWithExtras(Markdown):
+ """A markdowner class that enables most extras:
+
+ - footnotes
+ - code-color (only has effect if 'pygments' Python module on path)
+
+ These are not included:
+ - pyshell (specific to Python-related documenting)
+ - code-friendly (because it *disables* part of the syntax)
+ - link-patterns (because you need to specify some actual
+ link-patterns anyway)
+ """
+ extras = ["footnotes", "code-color"]
+
+
+#---- internal support functions
+
+class UnicodeWithAttrs(unicode):
+ """A subclass of unicode used for the return value of conversion to
+ possibly attach some attributes. E.g. the "toc_html" attribute when
+ the "toc" extra is used.
+ """
+ _toc = None
+ @property
+ def toc_html(self):
+ """Return the HTML for the current TOC.
+
+ This expects the `_toc` attribute to have been set on this instance.
+ """
+ if self._toc is None:
+ return None
+
+ def indent():
+ return ' ' * (len(h_stack) - 1)
+ lines = []
+ h_stack = [0] # stack of header-level numbers
+ for level, id, name in self._toc:
+ if level > h_stack[-1]:
+ lines.append("%s<ul>" % indent())
+ h_stack.append(level)
+ elif level == h_stack[-1]:
+ lines[-1] += "</li>"
+ else:
+ while level < h_stack[-1]:
+ h_stack.pop()
+ if not lines[-1].endswith("</li>"):
+ lines[-1] += "</li>"
+ lines.append("%s</ul></li>" % indent())
+ lines.append(u'%s<li><a href="#%s">%s</a>' % (
+ indent(), id, name))
+ while len(h_stack) > 1:
+ h_stack.pop()
+ if not lines[-1].endswith("</li>"):
+ lines[-1] += "</li>"
+ lines.append("%s</ul>" % indent())
+ return '\n'.join(lines) + '\n'
+
+
+_slugify_strip_re = re.compile(r'[^\w\s-]')
+_slugify_hyphenate_re = re.compile(r'[-\s]+')
+def _slugify(value):
+ """
+ Normalizes string, converts to lowercase, removes non-alpha characters,
+ and converts spaces to hyphens.
+
+ From Django's "django/template/defaultfilters.py".
+ """
+ import unicodedata
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
+ value = unicode(_slugify_strip_re.sub('', value).strip().lower())
+ return _slugify_hyphenate_re.sub('-', value)
+
+# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
+def _curry(*args, **kwargs):
+ function, args = args[0], args[1:]
+ def result(*rest, **kwrest):
+ combined = kwargs.copy()
+ combined.update(kwrest)
+ return function(*args + rest, **combined)
+ return result
+
+# Recipe: regex_from_encoded_pattern (1.0)
+def _regex_from_encoded_pattern(s):
+ """'foo' -> re.compile(re.escape('foo'))
+ '/foo/' -> re.compile('foo')
+ '/foo/i' -> re.compile('foo', re.I)
+ """
+ if s.startswith('/') and s.rfind('/') != 0:
+ # Parse it: /PATTERN/FLAGS
+ idx = s.rfind('/')
+ pattern, flags_str = s[1:idx], s[idx+1:]
+ flag_from_char = {
+ "i": re.IGNORECASE,
+ "l": re.LOCALE,
+ "s": re.DOTALL,
+ "m": re.MULTILINE,
+ "u": re.UNICODE,
+ }
+ flags = 0
+ for char in flags_str:
+ try:
+ flags |= flag_from_char[char]
+ except KeyError:
+ raise ValueError("unsupported regex flag: '%s' in '%s' "
+ "(must be one of '%s')"
+ % (char, s, ''.join(flag_from_char.keys())))
+ return re.compile(s[1:idx], flags)
+ else: # not an encoded regex
+ return re.compile(re.escape(s))
+
+# Recipe: dedent (0.1.2)
+def _dedentlines(lines, tabsize=8, skip_first_line=False):
+ """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
+
+ "lines" is a list of lines to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ Same as dedent() except operates on a sequence of lines. Note: the
+ lines list is modified **in-place**.
+ """
+ DEBUG = False
+ if DEBUG:
+ print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+ % (tabsize, skip_first_line)
+ indents = []
+ margin = None
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ indent = 0
+ for ch in line:
+ if ch == ' ':
+ indent += 1
+ elif ch == '\t':
+ indent += tabsize - (indent % tabsize)
+ elif ch in '\r\n':
+ continue # skip all-whitespace lines
+ else:
+ break
+ else:
+ continue # skip all-whitespace lines
+ if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
+ if margin is None:
+ margin = indent
+ else:
+ margin = min(margin, indent)
+ if DEBUG: print "dedent: margin=%r" % margin
+
+ if margin is not None and margin > 0:
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line: continue
+ removed = 0
+ for j, ch in enumerate(line):
+ if ch == ' ':
+ removed += 1
+ elif ch == '\t':
+ removed += tabsize - (removed % tabsize)
+ elif ch in '\r\n':
+ if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
+ lines[i] = lines[i][j:]
+ break
+ else:
+ raise ValueError("unexpected non-whitespace char %r in "
+ "line %r while removing %d-space margin"
+ % (ch, line, margin))
+ if DEBUG:
+ print "dedent: %r: %r -> removed %d/%d"\
+ % (line, ch, removed, margin)
+ if removed == margin:
+ lines[i] = lines[i][j+1:]
+ break
+ elif removed > margin:
+ lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+ break
+ else:
+ if removed:
+ lines[i] = lines[i][removed:]
+ return lines
+
+def _dedent(text, tabsize=8, skip_first_line=False):
+ """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
+
+ "text" is the text to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ textwrap.dedent(s), but don't expand tabs to spaces
+ """
+ lines = text.splitlines(1)
+ _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
+ return ''.join(lines)
+
+
+class _memoized(object):
+ """Decorator that caches a function's return value each time it is called.
+ If called later with the same arguments, the cached value is returned, and
+ not re-evaluated.
+
+ http://wiki.python.org/moin/PythonDecoratorLibrary
+ """
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+ def __call__(self, *args):
+ try:
+ return self.cache[args]
+ except KeyError:
+ self.cache[args] = value = self.func(*args)
+ return value
+ except TypeError:
+ # uncachable -- for instance, passing a list as an argument.
+ # Better to not cache than to blow up entirely.
+ return self.func(*args)
+ def __repr__(self):
+ """Return the function's docstring."""
+ return self.func.__doc__
+
+
+def _xml_oneliner_re_from_tab_width(tab_width):
+ """Standalone XML processing instruction regex."""
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,%d}
+ (?:
+ <\?\w+\b\s+.*?\?> # XML processing instruction
+ |
+ <\w+:\w+\b\s+.*?/> # namespaced single tag
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
+
+def _hr_tag_re_from_tab_width(tab_width):
+ return re.compile(r"""
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in \1
+ [ ]{0,%d}
+ <(hr) # start tag = \2
+ \b # word break
+ ([^<>])*? #
+ /?> # the matching end tag
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ """ % (tab_width - 1), re.X)
+_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
+
+
+def _xml_encode_email_char_at_random(ch):
+ r = random()
+ # Roughly 10% raw, 45% hex, 45% dec.
+ # '@' *must* be encoded. I [John Gruber] insist.
+ # Issue 26: '_' must be encoded.
+ if r > 0.9 and ch not in "@_":
+ return ch
+ elif r < 0.45:
+ # The [1:] is to drop leading '0': 0x63 -> x63
+ return '&#%s;' % hex(ord(ch))[1:]
+ else:
+ return '&#%s;' % ord(ch)
+
+
+
+#---- mainline
+
+class _NoReflowFormatter(optparse.IndentedHelpFormatter):
+ """An optparse formatter that does NOT reflow the description."""
+ def format_description(self, description):
+ return description or ""
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+def main(argv=None):
+ if argv is None:
+ argv = sys.argv
+ if not logging.root.handlers:
+ logging.basicConfig()
+
+ usage = "usage: %prog [PATHS...]"
+ version = "%prog "+__version__
+ parser = optparse.OptionParser(prog="markdown2", usage=usage,
+ version=version, description=cmdln_desc,
+ formatter=_NoReflowFormatter())
+ parser.add_option("-v", "--verbose", dest="log_level",
+ action="store_const", const=logging.DEBUG,
+ help="more verbose output")
+ parser.add_option("--encoding",
+ help="specify encoding of text content")
+ parser.add_option("--html4tags", action="store_true", default=False,
+ help="use HTML 4 style for empty element tags")
+ parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode",
+ help="sanitize literal HTML: 'escape' escapes "
+ "HTML meta chars, 'replace' replaces with an "
+ "[HTML_REMOVED] note")
+ parser.add_option("-x", "--extras", action="append",
+ help="Turn on specific extra features (not part of "
+ "the core Markdown spec). See above.")
+ parser.add_option("--use-file-vars",
+ help="Look for and use Emacs-style 'markdown-extras' "
+ "file var to turn on extras. See "
+ "<http://code.google.com/p/python-markdown2/wiki/Extras>.")
+ parser.add_option("--link-patterns-file",
+ help="path to a link pattern file")
+ parser.add_option("--self-test", action="store_true",
+ help="run internal self-tests (some doctests)")
+ parser.add_option("--compare", action="store_true",
+ help="run against Markdown.pl as well (for testing)")
+ parser.set_defaults(log_level=logging.INFO, compare=False,
+ encoding="utf-8", safe_mode=None, use_file_vars=False)
+ opts, paths = parser.parse_args()
+ log.setLevel(opts.log_level)
+
+ if opts.self_test:
+ return _test()
+
+ if opts.extras:
+ extras = {}
+ for s in opts.extras:
+ splitter = re.compile("[,;: ]+")
+ for e in splitter.split(s):
+ if '=' in e:
+ ename, earg = e.split('=', 1)
+ try:
+ earg = int(earg)
+ except ValueError:
+ pass
+ else:
+ ename, earg = e, None
+ extras[ename] = earg
+ else:
+ extras = None
+
+ if opts.link_patterns_file:
+ link_patterns = []
+ f = open(opts.link_patterns_file)
+ try:
+ for i, line in enumerate(f.readlines()):
+ if not line.strip(): continue
+ if line.lstrip().startswith("#"): continue
+ try:
+ pat, href = line.rstrip().rsplit(None, 1)
+ except ValueError:
+ raise MarkdownError("%s:%d: invalid link pattern line: %r"
+ % (opts.link_patterns_file, i+1, line))
+ link_patterns.append(
+ (_regex_from_encoded_pattern(pat), href))
+ finally:
+ f.close()
+ else:
+ link_patterns = None
+
+ from os.path import join, dirname, abspath, exists
+ markdown_pl = join(dirname(dirname(abspath(__file__))), "test",
+ "Markdown.pl")
+ for path in paths:
+ if opts.compare:
+ print "==== Markdown.pl ===="
+ perl_cmd = 'perl %s "%s"' % (markdown_pl, path)
+ o = os.popen(perl_cmd)
+ perl_html = o.read()
+ o.close()
+ sys.stdout.write(perl_html)
+ print "==== markdown2.py ===="
+ html = markdown_path(path, encoding=opts.encoding,
+ html4tags=opts.html4tags,
+ safe_mode=opts.safe_mode,
+ extras=extras, link_patterns=link_patterns,
+ use_file_vars=opts.use_file_vars)
+ sys.stdout.write(
+ html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
+ if extras and "toc" in extras:
+ log.debug("toc_html: " +
+ html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
+ if opts.compare:
+ test_dir = join(dirname(dirname(abspath(__file__))), "test")
+ if exists(join(test_dir, "test_markdown2.py")):
+ sys.path.insert(0, test_dir)
+ from test_markdown2 import norm_html_from_html
+ norm_html = norm_html_from_html(html)
+ norm_perl_html = norm_html_from_html(perl_html)
+ else:
+ norm_html = html
+ norm_perl_html = perl_html
+ print "==== match? %r ====" % (norm_perl_html == norm_html)
+
+
+if __name__ == "__main__":
+ sys.exit( main(sys.argv) )
+
diff --git a/cgi/oekaki.py b/cgi/oekaki.py
new file mode 100644
index 0000000..f0bada7
--- /dev/null
+++ b/cgi/oekaki.py
@@ -0,0 +1,176 @@
+# coding=utf-8
+import _mysql
+import os
+import cgi
+import random
+
+from database import *
+from settings import Settings
+from framework import *
+from formatting import *
+from template import *
+from post import *
+
+def oekaki(self, path_split):
+ """
+ Este script hace todo lo que tiene que hacer con los
+ archivos de Oekaki.
+ """
+ page = ''
+ skiptemplate = False
+
+ if len(path_split) > 2:
+ # Inicia el applet. Lo envia luego a este mismo script, a "Finish".
+ if path_split[2] == 'paint':
+ # Veamos que applet usar
+ applet = self.formdata['oek_applet'].split('|')
+
+ applet_name = applet[0]
+
+ if len(applet) > 1 and applet[1] == 'y':
+ applet_str = 'pro'
+ else:
+ applet_str = ''
+
+ if len(applet) > 2 and applet[2] == 'y':
+ use_selfy = True
+ else:
+ use_selfy = False
+
+ # Obtenemos el board
+ board = setBoard(self.formdata['board'])
+
+ if board['allow_oekaki'] != '1':
+ raise UserError, 'Esta sección no soporta oekaki.'
+
+ # Veamos a quien le estamos respondiendo
+ try:
+ parentid = int(self.formdata['parent'])
+ except:
+ parentid = 0
+
+ # Vemos si el usuario quiere una animacion
+ if 'oek_animation' in self.formdata.keys():
+ animation = True
+ animation_str = 'animation'
+ else:
+ animation = False
+ animation_str = ''
+
+ # Nos aseguramos que la entrada es numerica
+ try:
+ width = int(self.formdata['oek_x'])
+ height = int(self.formdata['oek_y'])
+ except:
+ raise UserError, 'Valores de tamaño inválidos (%s)' % repr(self.formdata)
+
+ params = {
+ 'dir_resource': Settings.BOARDS_URL + 'oek_temp/',
+ 'tt.zip': 'tt_def.zip',
+ 'res.zip': 'res.zip',
+ 'MAYSCRIPT': 'true',
+ 'scriptable': 'true',
+ 'tools': applet_str,
+ 'layer_count': '5',
+ 'undo': '90',
+ 'undo_in_mg': '15',
+ 'url_save': Settings.BOARDS_URL + 'oek_temp/save.php?applet=shi'+applet_str,
+ 'poo': 'false',
+ 'send_advance': 'true',
+ 'send_language': 'utf8',
+ 'send_header': '',
+ 'send_header_image_type': 'false',
+ 'thumbnail_type': animation_str,
+ 'image_jpeg': 'false',
+ 'image_size': '92',
+ 'compress_level': '4'
+ }
+
+ if 'oek_edit' in self.formdata.keys():
+ # Si hay que editar, cargar la imagen correspondiente en el canvas
+ pid = int(self.formdata['oek_edit'])
+ post = FetchOne('SELECT id, file, image_width, image_height FROM posts WHERE id = %d AND boardid = %s' % (pid, board['id']))
+ editfile = Settings.BOARDS_URL + board['dir'] + '/src/' + post['file']
+
+ params['image_canvas'] = edit
+ params['image_width'] = file['image_width']
+ params['image_height'] = file['image_height']
+ width = int(file['image_width'])
+ height = int(file['image_height'])
+ else:
+ editfile = None
+ params['image_width'] = str(width)
+ params['image_height'] = str(height)
+
+ if 'canvas' in self.formdata.keys():
+ editfile = self.formdata['canvas']
+
+ # Darle las dimensiones al exit script
+ params['url_exit'] = Settings.CGI_URL + 'oekaki/finish/' + board['dir'] + '/' + str(parentid)
+
+ page += renderTemplate("paint.html", {'applet': applet_name, 'edit': editfile, 'replythread': parentid, 'width': width, 'height': height, 'params': params, 'selfy': use_selfy})
+ elif path_split[2] == 'finish':
+ # path splits:
+ # 3: Board
+ # 4: Parentid
+ if path_split > 7:
+ # Al terminar de dibujar, llegamos aqui. Damos la opcion de postearlo.
+ board = setBoard(path_split[3])
+ try:
+ parentid = int(path_split[4])
+ except:
+ parentid = None
+
+ ts = int(time.time())
+ ip = inet_aton(self.environ["REMOTE_ADDR"])
+ fname = "%s/oek_temp/%d.png" % (Settings.HOME_DIR, ip)
+ oek = 'no'
+
+ if 'filebase' in self.formdata:
+ img = self.formdata['filebase']
+ if img.startswith("data:image/png;base64,"):
+ img = img[22:]
+ img = img.replace(' ', '+')
+ img = img.decode('base64')
+ with open(fname, 'wb') as f:
+ f.write(img)
+
+ if os.path.isfile(fname):
+ oek = ip
+
+ try:
+ timetaken = timestamp() - int(path_split[5][:-2])
+ except:
+ timetaken = 0
+
+ page += renderTemplate("board.html", {"threads": None, "oek_finish": oek, "replythread": parentid, "ts": ts})
+
+ elif path_split[2] == 'animation':
+ try:
+ board = setBoard(path_split[3])
+ file = int(path_split[4])
+ except:
+ raise UserError, 'Board o archivo de animación inválido.'
+
+ params = {
+ 'pch_file': Settings.BOARDS_URL + board['dir'] + '/src/' + str(file) + '.pch',
+ 'run': 'true',
+ 'buffer_progress': 'false',
+ 'buffer_canvas': 'true',
+ 'speed': '2',
+ 'res.zip': Settings.BOARDS_URL + 'oek_temp/res/' +'res.zip',
+ 'tt.zip': Settings.BOARDS_URL + 'oek_temp/res/' + 'tt.zip',
+ 'tt_size': '31'
+ }
+ page += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' + \
+ '<html xmlns="http://www.w3.org/1999/xhtml">\n<head><style type="text/css">html, body{margin: 0; padding: 0;height:100%;} .full{width:100%;height:100%;}</style>\n<title>Bienvenido a Internet | Oekaki</title>\n</head>\n' + \
+ '<body bgcolor="#CFCFFF" text="#800000" link="#003399" vlink="#808080" alink="#11FF11">\n' + \
+ '<table cellpadding="0" cellspacing="0" class="full"><tr><td class="full">\n'
+ page += '<applet name="pch" code="pch2.PCHViewer.class" archive="' + Settings.BOARDS_URL + 'oek_temp/PCHViewer123.jar" width="100%" height="100%">'
+ for key in params.keys():
+ page += '<param name="' + key + '" value="' + cleanString(params[key]) + '" />' + "\n"
+ page += '<div align="center">Java must be installed and enabled to use this applet. Please refer to our Java setup tutorial for more information.</div>'
+ page += '</applet>\n</td></tr></table>\n</body>\n</html>'
+
+ if not skiptemplate:
+ self.output = page
diff --git a/cgi/post.py b/cgi/post.py
new file mode 100644
index 0000000..f0ee814
--- /dev/null
+++ b/cgi/post.py
@@ -0,0 +1,1260 @@
+# coding=utf-8
+import math
+import os
+import shutil
+import time
+import threading
+import Queue
+import _mysql
+import formatting
+
+from database import *
+from template import *
+from settings import Settings
+from framework import *
+
+class Post(object):
+ def __init__(self, boardid=0):
+ self.post = {
+ "boardid": boardid,
+ "parentid": 0,
+ "name": "",
+ "tripcode": "",
+ "email": "",
+ "subject": "",
+ "message": "",
+ "password": "",
+ "file": "",
+ "file_hex": "",
+ "file_size": 0,
+ "thumb": "",
+ "image_width": 0,
+ "image_height": 0,
+ "thumb_width": 0,
+ "thumb_height": 0,
+ "ip": "",
+ "timestamp_formatted": "",
+ "timestamp": 0,
+ "bumped": 0,
+ "locked": 0,
+ }
+
+ def __getitem__(self, key):
+ return self.post[key]
+
+ def __setitem__(self, key, value):
+ self.post[key] = value
+
+ def __iter__(self):
+ return self.post
+
+ def insert(self):
+ logTime("Insertando Post")
+ post_values = [_mysql.escape_string(str(value)) for key, value in self.post.iteritems()]
+
+ return InsertDb("INSERT INTO `posts` (`%s`) VALUES ('%s')" % (
+ "`, `".join(self.post.keys()),
+ "', '".join(post_values)
+ ))
+
+class RegenerateThread(threading.Thread):
+ def __init__(self, threadid, request_queue):
+ threading.Thread.__init__(self, name="RegenerateThread-%d" % (threadid,))
+ self.request_queue = request_queue
+ self.board = Settings._.BOARD
+
+ def run(self):
+ Settings._.BOARD = self.board
+ while 1:
+ action = self.request_queue.get()
+ if action is None:
+ break
+ if action == "front":
+ regenerateFrontPages()
+ else:
+ regenerateThreadPage(action)
+
+def threadNumReplies(post):
+ """
+ Get how many replies a thread has
+ """
+ board = Settings._.BOARD
+
+ num = FetchOne("SELECT COUNT(1) FROM `posts` WHERE `parentid` = '%s' AND `boardid` = '%s'" % (post, board['id']), 0)
+ return int(num[0])+1
+
+def get_parent_post(post_id, board_id):
+ post = FetchOne("SELECT `id`, `email`, `message`, `locked`, `subject`, `timestamp`, `bumped`, `last`, `length` FROM `posts` WHERE `id` = %s AND `parentid` = 0 AND `IS_DELETED` = 0 AND `boardid` = %s LIMIT 1" % (post_id, board_id))
+ if post:
+ return post
+ else:
+ raise UserError, _("The ID of the parent post is invalid.")
+
+def getThread(postid=0, mobile=False, timestamp=0):
+ board = Settings._.BOARD
+ total_bytes = 0
+
+ database_lock.acquire()
+ try:
+ if timestamp:
+ cond = "`timestamp` = %s" % str(timestamp)
+ else:
+ cond = "`id` = %s" % str(postid)
+
+ op_post = FetchOne("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE %s AND `boardid` = %s AND parentid = 0 LIMIT 1" % (cond, board["id"]))
+ if op_post:
+ op_post['num'] = 1
+ if mobile:
+ op_post['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", op_post['timestamp_formatted'])
+ thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0}
+ #thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0, "omitted_img": 0}
+ total_bytes += len(op_post["message"])+80
+
+ replies = FetchAll("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE `parentid` = %s AND `boardid` = %s ORDER BY `id` ASC" % (op_post["id"], board["id"]))
+ thread["length"] = 1
+ if replies:
+ for reply in replies:
+ thread["length"] += 1
+ reply['num'] = thread["length"]
+ if mobile:
+ reply['message'] = formatting.fixMobileLinks(reply['message'])
+ reply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", reply['timestamp_formatted'])
+ thread["posts"].append(reply)
+ total_bytes += len(reply["message"])+57
+
+ # An imageboard needs subject
+ if board["board_type"] in ['1', '5']:
+ thread["timestamp"] = op_post["timestamp"]
+ thread["subject"] = op_post["subject"]
+ thread["message"] = op_post["message"]
+ thread["locked"] = op_post["locked"]
+ thread["size"] = "%d KB" % int(total_bytes / 1000)
+
+ #threads = [thread]
+ else:
+ return None
+ finally:
+ database_lock.release()
+
+ return thread
+
+def getID(threadid, postnum):
+ board = Settings._.BOARD
+
+ database_lock.acquire()
+ try:
+ posts = FetchAll("SELECT id FROM `posts` WHERE `parentid`=%s AND `boardid`=%s ORDER BY `id` ASC" % (thread["id"], board["id"]))
+ if posts:
+ post = posts[int(postnum)-1]
+ postid = post["id"]
+ else:
+ return None
+ finally:
+ database_lock.release()
+
+ return postid
+
+def shortenMsg(message, elid='0', elboard='0'):
+ """
+ Intenta acortar el mensaje si es necesario
+ Algoritmo traducido desde KusabaX
+ """
+ board = Settings._.BOARD
+
+ limit = 100 * int(board['numline'])
+
+ message_exploded = message.split('<br />')
+ if len(message) > limit or len(message_exploded) > int(board['numline']):
+ message_shortened = ''
+ for i in range(int(board['numline'])):
+ if i >= len(message_exploded):
+ break
+
+ message_shortened += message_exploded[i] + '<br />'
+
+ #try:
+ message_shortened = message_shortened.decode('utf-8', 'replace')
+ #except:
+
+ if len(message_shortened) > limit:
+ message_shortened = message_shortened[:limit]
+
+ message_shortened = formatting.close_html(message_shortened)
+
+ return True, message_shortened
+ else:
+ return False, message
+
+def threadUpdated(postid):
+ """
+ Shortcut to update front pages and thread page by passing a thread ID. Uses
+ the simple threading module to do both regenerateFrontPages() and
+ regenerateThreadPage() asynchronously
+ """
+ # Use queues only if multithreading is enabled
+ if Settings.USE_MULTITHREADING:
+ request_queue = Queue.Queue()
+ threads = [RegenerateThread(i, request_queue) for i in range(2)]
+ for t in threads:
+ t.start()
+
+ request_queue.put("front")
+ request_queue.put(postid)
+
+ for i in range(2):
+ request_queue.put(None)
+
+ for t in threads:
+ t.join
+ else:
+ regenerateFrontPages()
+ regenerateThreadPage(postid)
+
+def regenerateFrontPages():
+ """
+ Regenerates index.html and #.html for each page after that according to the number
+ of live threads in the database
+ """
+ board = Settings._.BOARD
+ threads = []
+ if board['board_type'] == '1':
+ threads_to_fetch = int(board['numthreads'])
+ threads_to_limit = threads_to_fetch + 50
+ else:
+ if board['dir'] == 'o':
+ threads_to_fetch = threads_to_limit = int(board['numthreads'])*21
+ else:
+ threads_to_fetch = threads_to_limit = int(board['numthreads'])*11
+
+ database_lock.acquire()
+ try:
+ # fetch necessary threads and calculate how many posts we need
+ allthreads_query = "SELECT id, timestamp, subject, locked, length FROM `posts` WHERE `boardid` = '%s' AND parentid = 0 AND IS_DELETED = 0 ORDER BY `bumped` DESC, `id` ASC LIMIT %d" % \
+ (board["id"], threads_to_limit)
+ allthreads = FetchAll(allthreads_query)
+ posts_to_fetch = 0
+ for t in allthreads[:threads_to_fetch]:
+ posts_to_fetch += int(t["length"])
+ more_threads = allthreads[threads_to_fetch:50]
+
+ # get the needed posts for the front page and order them
+ posts_query = "SELECT * FROM `posts` WHERE `boardid` = '%s' ORDER BY `bumped` DESC, CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC LIMIT %d" % \
+ (board["id"], posts_to_fetch)
+ posts = FetchAll(posts_query)
+
+ threads = []
+ if posts:
+ thread = None
+ post_num = 0
+
+ for post in posts:
+ if post["parentid"] == '0':
+ skipThread = False
+ if post["IS_DELETED"] == '0':
+ # OP; Make new thread
+ if thread is not None:
+ thread["length"] = post_num
+ threads.append(thread)
+ post_num = post["num"] = 1
+ thread = {"id": post["id"], "timestamp": post["timestamp"], "subject": post["subject"], "locked": post["locked"], "posts": [post]}
+ else:
+ skipThread = True
+ else:
+ if not skipThread:
+ post_num += 1
+ post["num"] = post_num
+ thread["posts"].append(post)
+
+ if post_num:
+ thread["length"] = post_num
+ threads.append(thread)
+ finally:
+ database_lock.release()
+
+ pages = []
+ is_omitted = False
+ if len(threads) > 0:
+ # Todo : Make this better
+ if board['board_type'] == '1':
+ page_count = 1 # Front page only
+ threads_per_page = int(board['numthreads'])
+ else:
+ if board['dir'] == 'o':
+ front_limit = int(board['numthreads'])*21
+ else:
+ front_limit = int(board['numthreads'])*11
+
+ if len(threads) >= front_limit:
+ is_omitted = True
+
+ page_count = int(math.ceil(float(len(threads)) / float(int(board['numthreads']))))
+ threads_per_page = int(board['numthreads'])
+
+ for i in xrange(page_count):
+ pages.append([])
+ start = i * threads_per_page
+ end = start + threads_per_page
+ for thread in threads[start:end]:
+ pages[i].append(thread)
+ else:
+ page_count = 0
+ is_omitted = False
+ pages.append({})
+
+ page_num = 0
+ for pagethreads in pages:
+ regeneratePage(page_num, page_count, pagethreads, is_omitted, more_threads)
+ page_num += 1
+
+def regeneratePage(page_num, page_count, threads, is_omitted=False, more_threads=[]):
+ """
+ Regenerates a single page and writes it to .html
+ """
+ board = Settings._.BOARD
+
+ for thread in threads:
+ replylimit = int(board['numcont'])
+
+ # Create reply list
+ parent = thread["posts"].pop(0)
+ replies = thread["posts"]
+ thread["omitted"] = 0
+ #thread["omitted_img"] = 0
+
+ # Omit posts
+ while(len(replies) > replylimit):
+ post = replies.pop(0)
+ thread["omitted"] += 1
+ #if post["file"]:
+ # thread["omitted_img"] += 1
+
+ # Remake thread with necessary replies only
+ replies.insert(0, parent)
+ thread["posts"] = replies
+
+ # Shorten messages
+ for post in thread["posts"]:
+ post["shortened"], post["message"] = shortenMsg(post["message"])
+
+ # Build page according to page number
+ if page_num == 0:
+ file_name = "index"
+ else:
+ file_name = str(page_num)
+
+ if board['board_type'] == '1':
+ templatename = "txt_board.html"
+ else:
+ templatename = "board.html"
+
+ page_rendered = renderTemplate(templatename, {"threads": threads, "pagenav": pageNavigator(page_num, page_count, is_omitted), "more_threads": more_threads})
+
+ f = open(Settings.ROOT_DIR + board["dir"] + "/" + file_name + ".html", "w")
+ try:
+ f.write(page_rendered)
+ finally:
+ f.close()
+
+def threadList(mode=0):
+ board = Settings._.BOARD
+
+ if mode == 1:
+ mobile = True
+ maxthreads = 20
+ cutFactor = 100
+ elif mode == 2:
+ mobile = True
+ maxthreads = 1000
+ cutFactor = 50
+ elif mode == 3:
+ mobile = True
+ maxthreads = 1000
+ cutFactor = 100
+ else:
+ mobile = False
+ maxthreads = 1000
+ cutFactor = 70
+
+ if board['board_type'] == '1':
+ filename = "txt_threadlist.html"
+ full_threads = FetchAll("SELECT id, timestamp, timestamp_formatted, subject, length, last FROM `posts` WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \
+ % {'board': board["id"], 'limit': maxthreads})
+ else:
+ filename = "threadlist.html"
+ full_threads = FetchAll("SELECT p.*, coalesce(x.count,1) AS length, coalesce(x.t,p.timestamp) AS last FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1)+1 as count, max(timestamp) as t FROM `posts` " +\
+ "WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s AND p.IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \
+ % {'board': board["id"], 'limit': maxthreads})
+
+ # Generate threadlist
+ timestamps = []
+ for thread in full_threads:
+ if board['board_type'] == '1':
+ thread["timestamp_formatted"] = thread["timestamp_formatted"].split(" ")[0]
+ timestamps.append([thread["last"], formatTimestamp(thread["last"])])
+ if mobile:
+ timestamps[-1][1] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", timestamps[-1][1])
+ else:
+ if len(thread['message']) > cutFactor:
+ thread['shortened'] = True
+ else:
+ thread['shortened'] = False
+ thread['message'] = thread['message'].replace('<br />', ' ')
+ thread['message'] = thread['message'].split("<hr />")[0]
+ thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message'])
+ thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8')
+ thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities
+ thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted'])
+
+ # Get last reply if in mobile mode
+ if mode == 1:
+ thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted'])
+ lastreply = FetchOne("SELECT * FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id']))
+ if lastreply:
+ if len(lastreply['message']) > 60:
+ lastreply['shortened'] = True
+ else:
+ lastreply['shortened'] = False
+ lastreply['message'] = lastreply['message'].replace('<br />', ' ')
+ lastreply['message'] = lastreply['message'].split("<hr />")[0]
+ lastreply['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', lastreply['message'])
+ lastreply['message'] = lastreply['message'].decode('utf-8')[:60].encode('utf-8')
+ lastreply['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', lastreply['message']) # Removes incomplete HTML entities
+ lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted'])
+ thread["lastreply"] = lastreply
+ else:
+ thread["lastreply"] = None
+ elif mode == 2:
+ lastreply = FetchOne("SELECT timestamp_formatted FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id']))
+ if lastreply:
+ lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted'])
+ thread["lastreply"] = lastreply
+
+ return renderTemplate(filename, {"more_threads": full_threads, "timestamps": timestamps, "mode": mode}, mobile)
+
+def catalog(sort=''):
+ board = Settings._.BOARD
+
+ if board['board_type'] != '0':
+ raise UserError, "No hay catálogo disponible para esta sección."
+
+ cutFactor = 500
+
+ q_sort = '`bumped` DESC, `id` ASC'
+ if sort:
+ if sort == '1':
+ q_sort = '`timestamp` DESC'
+ elif sort == '2':
+ q_sort = '`timestamp` ASC'
+ elif sort == '3':
+ q_sort = '`length` DESC'
+ elif sort == '4':
+ q_sort = '`length` ASC'
+
+ threads = FetchAll("SELECT id, subject, message, length, thumb, expires_formatted FROM `posts` " +\
+ "WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY %(sort)s" \
+ % {'board': board["id"], 'sort': q_sort})
+
+ for thread in threads:
+ if len(thread['message']) > cutFactor:
+ thread['shortened'] = True
+ else:
+ thread['shortened'] = False
+ thread['message'] = thread['message'].replace('<br />', ' ')
+ thread['message'] = thread['message'].split("<hr />")[0]
+ thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message'])
+ thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8')
+ thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities
+
+ return renderTemplate("catalog.html", {"threads": threads, "i_sort": sort})
+
+def regenerateThreadPage(postid):
+ """
+ Regenerates /res/#.html for supplied thread id
+ """
+ board = Settings._.BOARD
+
+ thread = getThread(postid)
+
+ if board['board_type'] in ['1', '5']:
+ template_filename = "txt_thread.html"
+ outname = Settings.ROOT_DIR + board["dir"] + "/read/" + str(thread["timestamp"]) + ".html"
+ title_matome = thread['subject']
+ post_preview = cut_home_msg(thread['posts'][0]['message'], 0)
+ else:
+ template_filename = "board.html"
+ outname = Settings.ROOT_DIR + board["dir"] + "/res/" + str(postid) + ".html"
+ post_preview = cut_home_msg(thread['posts'][0]['message'], len(board['name']))
+
+ if thread['posts'][0]['subject'] != board['subject']:
+ title_matome = thread['posts'][0]['subject']
+ else:
+ title_matome = post_preview
+
+ page = renderTemplate(template_filename, {"threads": [thread], "replythread": postid, "matome": title_matome, "preview": post_preview}, False)
+
+ f = open(outname, "w")
+ try:
+ f.write(page)
+ finally:
+ f.close()
+
+def threadPage(postid, mobile=False, timestamp=0):
+ board = Settings._.BOARD
+
+ if board['board_type'] in ['1', '5']:
+ template_filename = "txt_thread.html"
+ else:
+ template_filename = "board.html"
+
+ threads = [getThread(postid, mobile, timestamp)]
+
+ return renderTemplate(template_filename, {"threads": threads, "replythread": postid}, mobile)
+
+def dynamicRead(parentid, ranges, mobile=False):
+ import re
+ board = Settings._.BOARD
+
+ if board['board_type'] != '1':
+ raise UserError, "Esta sección no es un BBS y como tal no soporta lectura dinámica."
+
+ # get entire thread
+ template_fname = "txt_thread.html"
+ thread = getThread(timestamp=parentid, mobile=mobile)
+
+ if not thread:
+ # Try the archive
+ fname = Settings.ROOT_DIR + board["dir"] + "/kako/" + str(parentid) + ".json"
+ if os.path.isfile(fname):
+ import json
+ with open(fname) as f:
+ thread = json.load(f)
+ thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']]
+ template_fname = "txt_archive.html"
+ else:
+ raise UserError, 'El hilo no existe.'
+
+ filtered_thread = {
+ "id": thread['id'],
+ "timestamp": thread['timestamp'],
+ "length": thread['length'],
+ "subject": thread['subject'],
+ "locked": thread['locked'],
+ "posts": [],
+ }
+
+ if 'size' in thread:
+ filtered_thread['size'] = thread['size']
+
+ no_op = False
+ if ranges.endswith('n'):
+ no_op = True
+ ranges = ranges[:-1]
+
+ # get thread length
+ total = thread["length"]
+
+ # compile regex
+ __multiple_ex = re.compile("^([0-9]*)-([0-9]*)$")
+ __single_ex = re.compile("^([0-9]+)$")
+ __last_ex = re.compile("^l([0-9]+)$")
+ start = 0
+ end = 0
+
+ # separate by commas (,)
+ for range in ranges.split(','):
+ # single post (#)
+ range_match = __single_ex.match(range)
+ if range_match:
+ postid = int(range_match.group(1))
+ if postid > 0 and postid <= total:
+ filtered_thread["posts"].append(thread["posts"][postid-1])
+
+ # go to next range
+ continue
+
+ # post range (#-#)
+ range_match = __multiple_ex.match(range)
+ if range_match:
+ start = int(range_match.group(1) or 1)
+ end = int(range_match.group(2) or total)
+
+ if start > total:
+ start = total
+ if end > total:
+ end = total
+
+ if start < end:
+ filtered_thread["posts"].extend(thread["posts"][start-1:end])
+ else:
+ list = thread["posts"][end-1:start]
+ list.reverse()
+ filtered_thread["posts"].extend(list)
+
+ # go to next range
+ continue
+
+ # last posts (l#)
+ range_match = __last_ex.match(range)
+ if range_match:
+ length = int(range_match.group(1))
+ start = total - length + 1
+ end = total
+ if start < 1:
+ start = 1
+
+ filtered_thread["posts"].extend(thread["posts"][start-1:])
+
+ continue
+
+ # calculate previous and next ranges
+ prevrange = None
+ nextrange = None
+ if __multiple_ex.match(ranges) or __last_ex.match(ranges):
+ if mobile:
+ range_n = 50
+ else:
+ range_n = 100
+
+ prev_start = start-range_n
+ prev_end = start-1
+ next_start = end+1
+ next_end = end+range_n
+
+ if prev_start < 1:
+ prev_start = 1
+ if next_end > total:
+ next_end = total
+
+ if start > 1:
+ prevrange = '%d-%d' % (prev_start, prev_end)
+ if end < total:
+ nextrange = '%d-%d' % (next_start, next_end)
+
+ if not no_op and start > 1 and end > 1:
+ filtered_thread["posts"].insert(0, thread["posts"][0])
+
+ if not filtered_thread["posts"]:
+ raise UserError, "No hay posts que mostrar."
+
+ post_preview = cut_home_msg(filtered_thread["posts"][0]["message"], 0)
+
+ return renderTemplate(template_fname, {"threads": [filtered_thread], "replythread": parentid, "prevrange": prevrange, "nextrange": nextrange, "preview": post_preview}, mobile, noindex=True)
+
+def regenerateBoard(everything=False):
+ """
+ Update front pages and every thread res HTML page
+ """
+ board = Settings._.BOARD
+ op_posts = []
+
+ if everything:
+ op_posts = FetchAll("SELECT `id` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0" % board["id"])
+
+ # Use queues only if multithreading is enabled
+ if Settings.USE_MULTITHREADING:
+ request_queue = Queue.Queue()
+ threads = [RegenerateThread(i, request_queue) for i in range(Settings.MAX_PROGRAM_THREADS)]
+ for t in threads:
+ t.start()
+
+ request_queue.put("front")
+
+ for post in op_posts:
+ request_queue.put(post["id"])
+
+ for i in range(Settings.MAX_PROGRAM_THREADS):
+ request_queue.put(None)
+
+ for t in threads:
+ t.join()
+ else:
+ regenerateFrontPages()
+ for post in op_posts:
+ regenerateThreadPage(post["id"])
+
+def deletePost(postid, password, deltype='0', imageonly=False, quick=False):
+ """
+ Remove post from database and unlink file (if present), along with all replies
+ if supplied post is a thread
+ """
+ board = Settings._.BOARD
+
+ # make sure postid is numeric
+ postid = int(postid)
+
+ # get post
+ post = FetchOne("SELECT `id`, `timestamp`, `parentid`, `file`, `thumb`, `password`, `length` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(postid)))
+
+ # abort if the post doesn't exist
+ if not post:
+ raise UserError, _("There isn't a post with this ID. It was probably deleted.")
+
+ if password:
+ if password != post['password']:
+ raise UserError, "No tienes permiso para eliminar este mensaje."
+ if post["parentid"] == '0' and int(post["length"]) >= Settings.DELETE_FORBID_LENGTH:
+ raise UserError, "No puedes eliminar un hilo con tantas respuestas."
+ if (int(time.time()) - int(post["timestamp"])) > 86400:
+ raise UserError, "No puedes eliminar un post tan viejo."
+
+ # just update the DB if deleting only the image, otherwise delete whole post
+ if imageonly:
+ if post["file"]:
+ deleteFile(post)
+
+ UpdateDb("UPDATE `posts` SET `file` = '', `file_hex` = '', `thumb` = '', `thumb_width` = 0, `thumb_height` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(post['id'])))
+ else:
+ if int(post["parentid"]) == 0:
+ deleteReplies(post)
+
+ logTime("Deleting post " + str(postid))
+ if deltype != '0' and post["parentid"] != '0':
+ # Soft delete (recycle bin)
+ UpdateDb("UPDATE `posts` SET `IS_DELETED` = %s WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (deltype, board["id"], post["id"]))
+ else:
+ # Hard delete
+ if post["file"]:
+ deleteFile(post)
+
+ UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], post["id"]))
+ if post['parentid'] != '0':
+ UpdateDb("UPDATE `posts` SET length = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (threadNumReplies(post["parentid"]), post["parentid"], board["id"]))
+
+ if post['parentid'] == '0':
+ if board['board_type'] == '1':
+ os.unlink(Settings.ROOT_DIR + board["dir"] + "/read/" + post["timestamp"] + ".html")
+ else:
+ os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + post["id"] + ".html")
+
+ regenerateHome()
+
+ # rebuild thread and fronts if reply; rebuild only fronts if not
+ if post["parentid"] != '0':
+ threadUpdated(post["parentid"])
+ else:
+ regenerateFrontPages()
+
+def deleteReplies(thread):
+ board = Settings._.BOARD
+
+ # delete files first
+ replies = FetchAll("SELECT `parentid`, `file`, `thumb` FROM `posts` WHERE `boardid` = %s AND `parentid` = %s AND `file` != ''" % (board["id"], thread["id"]))
+ for post in replies:
+ deleteFile(post)
+
+ # delete all replies from DB
+ UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `parentid` = %s" % (board["id"], thread["id"]))
+
+def deleteFile(post):
+ """
+ Unlink file and thumb of supplied post
+ """
+ board = Settings._.BOARD
+
+ try:
+ os.unlink(Settings.IMAGES_DIR + board["dir"] + "/src/" + post["file"])
+ except:
+ pass
+
+ # we don't want to delete mime thumbnails
+ if post["thumb"].startswith("mime"):
+ return
+
+ try:
+ os.unlink(Settings.IMAGES_DIR + board["dir"] + "/thumb/" + post["thumb"])
+ except:
+ pass
+
+ try:
+ os.unlink(Settings.IMAGES_DIR + board["dir"] + "/mobile/" + post["thumb"])
+ except:
+ pass
+
+ if int(post["parentid"]) == 0:
+ try:
+ os.unlink(Settings.IMAGES_DIR + board["dir"] + "/cat/" + post["thumb"])
+ except:
+ pass
+
+def trimThreads():
+ """
+ Delete any threads which have passed the MAX_THREADS setting
+ """
+ logTime("Trimming threads")
+ board = Settings._.BOARD
+ archived = False
+
+ # Use limit of the board type
+ if board['board_type'] == '1':
+ limit = Settings.TXT_MAX_THREADS
+ else:
+ limit = Settings.MAX_THREADS
+
+ # trim expiring threads first
+ if board['maxage'] != '0':
+ t = time.time()
+
+ alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT))
+ time_limit = t + (alert_time * 86400)
+ old_ops = FetchAll("SELECT `id`, `timestamp`, `expires`, `expires_alert`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `expires` > 0 AND `expires` < %s LIMIT 50" % (board['id'], time_limit))
+
+ for op in old_ops:
+ if t >= int(op['expires']):
+ # Trim old threads
+ if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH:
+ archiveThread(op["id"])
+ archived = True
+
+ deletePost(op["id"], None)
+ else:
+ # Add alert to threads approaching deletion
+ UpdateDb("UPDATE `posts` SET expires_alert = 1 WHERE `boardid` = %s AND `id` = %s" % (board['id'], op['id']))
+
+ # trim inactive threads next
+ if board['maxinactive'] != '0':
+ t = time.time()
+
+ oldest_last = t - (int(board['maxinactive']) * 86400)
+ old_ops = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `last` < %d LIMIT 50" % (board['id'], oldest_last))
+
+ for op in old_ops:
+ if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH:
+ archiveThread(op["id"])
+ archived = True
+
+ deletePost(op["id"], None)
+
+ # select trim type by board
+ if board['board_type'] == '1':
+ trim_method = Settings.TXT_TRIM_METHOD
+ else:
+ trim_method = Settings.TRIM_METHOD
+
+ # select order by trim
+ if trim_method == 1:
+ order = 'last DESC'
+ elif trim_method == 2:
+ order = 'bumped DESC'
+ else:
+ order = 'timestamp DESC'
+
+ # Trim the last thread
+ op_posts = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 ORDER BY %s" % (board["id"], order))
+ if len(op_posts) > limit:
+ posts = op_posts[limit:]
+ for post in posts:
+ if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH:
+ archiveThread(post["id"])
+ archived = True
+
+ deletePost(post["id"], None)
+ pass
+
+ if archived:
+ regenerateKako()
+
+def autoclose_thread(parentid, t, replies):
+ """
+ If the thread is crossing the reply limit, close it with a message.
+ """
+ board = Settings._.BOARD
+
+ # decide the replylimit
+ if board['board_type'] == '1' and Settings.TXT_CLOSE_THREAD_ON_REPLIES > 0:
+ replylimit = Settings.TXT_CLOSE_THREAD_ON_REPLIES
+ elif Settings.CLOSE_THREAD_ON_REPLIES > 0:
+ replylimit = Settings.CLOSE_THREAD_ON_REPLIES
+ else:
+ return # do nothing
+
+ # close it if passing replylimit
+ #if replies >= replylimit or board["dir"] == "polka":
+ if replies >= replylimit:
+ notice_post = Post(board["id"])
+ notice_post["parentid"] = parentid
+ notice_post["name"] = "Sistema"
+ notice_post["message"] = "El hilo ha llegado al límite de respuestas.<br />Si quieres continuarlo, por favor crea otro."
+ notice_post["timestamp"] = t+1
+ notice_post["bumped"] = get_parent_post(parentid, board["id"])["bumped"]
+ notice_post["timestamp_formatted"] = str(replylimit) + " mensajes"
+ notice_post.insert()
+ UpdateDb("UPDATE `posts` SET `locked` = 1 WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (board["id"], _mysql.escape_string(parentid)))
+
+def pageNavigator(page_num, page_count, is_omitted=False):
+ """
+ Create page navigator in the format of [0], [1], [2]...
+ """
+ board = Settings._.BOARD
+
+ # No threads?
+ if page_count == 0:
+ return ''
+
+ # TODO nijigen HACK
+ first_str = "Primera página"
+ last_str = "Última página"
+ previous_str = _("Previous")
+ next_str = _("Next")
+ omitted_str = "Resto omitido"
+
+ pagenav = "<span>"
+ if page_num == 0:
+ pagenav += first_str
+ else:
+ previous = str(page_num - 1)
+ if previous == "0":
+ previous = ""
+ else:
+ previous = previous + ".html"
+ pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + previous + '"><input value="'+previous_str+'" type="submit" /></form>'
+
+ pagenav += "</span><span>"
+
+ for i in xrange(page_count):
+ if i == page_num:
+ pagenav += "[<strong>%d</strong>]" % i
+ else:
+ if i == 0:
+ pagenav += '[<a href="%s%s/">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i)
+ else:
+ pagenav += '[<a href="%s%s/%d.html">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i, i)
+
+ if i > 0 and (i % 10) == 0 and not is_omitted:
+ pagenav += '<br />'
+ elif i < 10:
+ pagenav += '&nbsp;'
+
+ if is_omitted:
+ pagenav += "[" + omitted_str + "]"
+
+ pagenav += "<!-- "+repr(is_omitted)+"-->"
+ pagenav += "</span><span>"
+
+ next = (page_num + 1)
+ if next == page_count:
+ pagenav += last_str + "</span>"
+ else:
+ pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + str(next) + '.html"><input value="'+next_str+'" type="submit" /></form></span>'
+
+ return pagenav
+
+def flood_check(t,post,boardid):
+ board = Settings._.BOARD
+
+ if not post["parentid"]:
+ maxtime = t - int(board['threadsecs'])
+ #lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0)
+
+ # NO MATTER THE IP
+ lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (boardid, maxtime), 0)
+ pass
+ else:
+ maxtime = t - int(board['postsecs'])
+ lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` != 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0)
+
+ if int(lastpost[0]):
+ if post["parentid"]:
+ raise UserError, _("Flood detected. Please wait a moment before posting again.")
+ else:
+ lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid`=0 and `boardid`='%s' and IS_DELETED = 0 ORDER BY `timestamp` DESC" % (boardid), 0)
+ wait = int(int(board['threadsecs']) - (t - int(lastpost[0])))
+ raise UserError, "Por favor espera " + str(wait) + " segundos antes de crear otro hilo."
+
+def cut_home_msg(message, boardlength=0):
+ short_message = message.replace("<br />", " ")
+ short_message = short_message.split("<hr />")[0]
+ short_message = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes HTML tags
+ limit = Settings.HOME_LASTPOSTS_LENGTH - boardlength
+
+ if len(short_message) > limit:
+ if isinstance(short_message, unicode):
+ short_message = short_message[:limit].encode('utf-8') + "…"
+ else:
+ short_message = short_message.decode('utf-8')[:limit].encode('utf-8') + "…"
+ short_message = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes incomplete HTML
+ return short_message
+
+def getLastAge(limit):
+ threads = []
+ sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, bumped, last, length, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND posts.locked < 3 ORDER BY bumped DESC LIMIT %d" % limit
+ threads = FetchAll(sql)
+
+ for post in threads:
+ post['id'] = int(post['id'])
+ post['bumped'] = int(post['bumped'])
+ post['last'] = int(post['last'])
+ post['length'] = int(post['length'])
+ post['board_type'] = int(post['board_type'])
+ post['timestamp'] = int(post['timestamp'])
+ post['content'] = cut_home_msg(post['content'], 0)
+
+ if post['board_type'] == 1:
+ post['url'] = '/%s/read/%d/l10' % (post['dir'], post['timestamp'])
+ else:
+ post['url'] = '/%s/res/%d.html' % (post['dir'], post['id'])
+
+ return threads
+
+def getNewThreads(limit):
+ threads = []
+ sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND boards.id <> 34 AND boards.id <> 13 AND posts.locked = 0 ORDER BY timestamp DESC LIMIT %d" % (limit)
+ threads = FetchAll(sql)
+
+ for post in threads:
+ post['id'] = int(post['id'])
+ post['board_type'] = int(post['board_type'])
+ post['timestamp'] = int(post['timestamp'])
+ post['timestamp_formatted'] = formatTimestamp(post['timestamp'], True)
+ post['timestamp_formatted'] = post['timestamp_formatted'][:8] + ' ' + post['timestamp_formatted'][13:]
+ post['content'] = cut_home_msg(post['content'], 0)
+ if post['board_type'] == 1:
+ post['url'] = '/%s/read/%d' % (post['dir'], post['timestamp'])
+ else:
+ post['url'] = '/%s/res/%d.html' % (post['dir'], post['id'])
+
+ return threads
+
+def regenerateHome():
+ """
+ Update index.html in the boards directory with useful data for users
+ """
+ logTime("Updating home")
+ t = datetime.datetime.now()
+
+ limit = Settings.HOME_LASTPOSTS
+ template_values = {
+ 'header': Settings.SITE_TITLE,
+ 'slogan': Settings.SITE_SLOGAN,
+ 'latest_news': FetchAll("SELECT `timestamp`, `message`, `timestamp_formatted` FROM `news` WHERE `type` = '2' ORDER BY `timestamp` DESC LIMIT " + str(Settings.HOME_NEWS)),
+ 'latest_age': getLastAge(limit),
+ 'latest_age_num': limit,
+ 'new_threads': getNewThreads(Settings.HOME_NEWTHREADS),
+ }
+
+ page_rendered = renderTemplate('home.html', template_values)
+ f = open(Settings.HOME_DIR + "home.html", "w")
+ try:
+ f.write(page_rendered)
+ finally:
+ f.close()
+
+ if Settings.ENABLE_RSS:
+ sql = "SELECT id, boardid, board_name, timestamp, timestamp_formatted, content, url FROM last ORDER BY timestamp DESC LIMIT 10"
+ rss = FetchAll(sql)
+ rss_rendered = renderTemplate('home.rss', {'posts': rss})
+ f = open(Settings.HOME_DIR + "bai.rss", "w")
+ try:
+ f.write(rss_rendered)
+ finally:
+ f.close()
+
+def regenerateNews():
+ """
+ Update news.html in the boards directory with older news
+ """
+ posts = FetchAll("SELECT * FROM `news` WHERE `type` = '1' ORDER BY `timestamp` DESC")
+ template_values = {
+ 'title': 'Noticias',
+ 'posts': posts,
+ 'header': Settings.SITE_TITLE,
+ 'slogan': Settings.SITE_SLOGAN,
+ 'navbar': False,
+ }
+
+ page_rendered = renderTemplate('news.html', template_values)
+
+ f = open(Settings.HOME_DIR + "noticias.html", "w")
+ try:
+ f.write(page_rendered)
+ finally:
+ f.close()
+
+def regenerateAccess():
+ if not Settings.HTACCESS_GEN:
+ return False
+
+ bans = FetchAll("SELECT INET_NTOA(`ip`) AS 'ip', INET_NTOA(`netmask`) AS 'netmask', `boards` FROM `bans` WHERE `blind` = '1'")
+ listbans = dict()
+ #listbans_global = list()
+
+ boarddirs = FetchAll('SELECT `dir` FROM `boards`')
+ for board in boarddirs:
+ listbans[board['dir']] = list()
+
+ for ban in bans:
+ ipmask = ban["ip"]
+ if ban["netmask"] is not None:
+ ipmask += '/' + ban["netmask"]
+
+ if ban["boards"] != "":
+ boards = pickle.loads(ban["boards"])
+ for board in boards:
+ listbans[board].append(ipmask)
+ else:
+ #listbans_global.append(ban["ip"])
+ for board in boarddirs:
+ if board['dir'] not in Settings.EXCLUDE_GLOBAL_BANS:
+ listbans[board['dir']].append(ipmask)
+
+ # Generate .htaccess for each board
+ for board in listbans.keys():
+ template_values = {
+ 'ips': listbans[board],
+ 'dir': board,
+ }
+
+ page_rendered = renderTemplate('htaccess', template_values)
+ f = open(Settings.ROOT_DIR + board + "/.htaccess", "w")
+ try:
+ f.write(page_rendered)
+ finally:
+ f.close()
+
+ return True
+
+def regenerateKako():
+ board = Settings._.BOARD
+
+ threads = FetchAll("SELECT * FROM archive WHERE boardid = %s ORDER BY timestamp DESC" % board['id'])
+ page = renderTemplate('kako.html', {'threads': threads})
+ with open(Settings.ROOT_DIR + board["dir"] + "/kako/index.html", "w") as f:
+ f.write(page)
+
+def make_url(postid, post, parent_post, noko, mobile):
+ board = Settings._.BOARD
+
+ parentid = post["parentid"]
+ if not parentid:
+ parentid = postid
+
+ if mobile:
+ if not noko:
+ url = Settings.CGI_URL + 'mobile/' + board["dir"]
+ elif board["board_type"] == '1':
+ url = "%s/mobileread/%s/%s/l10#form" % (Settings.CGI_URL, board["dir"], parent_post['timestamp'])
+ else:
+ url = "%s/mobileread/%s/%s#%s" % (Settings.CGI_URL, board["dir"], parentid, postid)
+ else:
+ if not noko:
+ url = Settings.BOARDS_URL + board["dir"] + "/"
+ elif board["board_type"] == '1':
+ url = "%s/read/%s/l50#bottom" % (Settings.BOARDS_URL + board["dir"], str(parent_post['timestamp']))
+ else:
+ url = "%s/res/%s.html#%s" % (Settings.BOARDS_URL + board["dir"], str(parentid), postid)
+
+ return url
+
+def make_redirect(url, timetaken=None):
+ board = Settings._.BOARD
+ randomPhrase = getRandomLine('quotes.conf')
+
+ return renderTemplate('redirect.html', {'url': url, 'message': randomPhrase, 'timetaken': timetaken})
+
+def latestAdd(post, postnum, postid, parent_post):
+ board = Settings._.BOARD
+
+ #UpdateDb("DELETE FROM last LIMIT 15, 500#")
+
+ if post['subject'] and post['subject'] != board["subject"]:
+ content = post['subject']
+ else:
+ content = cut_home_msg(post['message'], len(board['name']))
+
+ timestamp_formatted = datetime.datetime.fromtimestamp(post['timestamp']).strftime('%Y-%m-%dT%H:%M:%S%Z')
+ parentid = parent_post['id'] if post['parentid'] else postid
+
+ if board['board_type'] == '1':
+ url = '/%s/read/%s/%d' % (board['dir'], (parent_post['timestamp'] if post['parentid'] else post['timestamp']), (postnum if postnum else 1))
+ else:
+ url = '/%s/res/%s.html#%s' % (board['dir'], parentid, postid)
+
+ sql = "INSERT INTO last (id, boardid, board_name, timestamp, timestamp_formatted, content, url) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (str(postid), board['id'], _mysql.escape_string(board['name']), post['timestamp'], _mysql.escape_string(timestamp_formatted), _mysql.escape_string(content), _mysql.escape_string(url))
+ UpdateDb(sql)
+
+def latestRemove(postid):
+ board = Settings._.BOARD
+ UpdateDb("DELETE FROM last WHERE id = %s AND boardid = %s" % (str(postid), board['id']))
+
+def archiveThread(postid):
+ import json
+ board = Settings._.BOARD
+
+ thread = getThread(postid, False)
+ post_preview = cut_home_msg(thread['posts'][0]['message'], 0)
+
+ page = renderTemplate("txt_archive.html", {"threads": [thread], "replythread": postid, "preview": post_preview}, False)
+ with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".html", "w") as f:
+ f.write(page)
+
+ thread['keys'] = ['num', 'IS_DELETED', 'name', 'tripcode', 'email', 'message', 'timestamp_formatted']
+ thread['posts'] = [[row[key] for key in thread['keys']] for row in thread['posts']]
+ try:
+ with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".json", "w") as f:
+ json.dump(thread, f, indent=0)
+ except:
+ raise UserError, "Can't archive: %s" % thread['timestamp']
+
+ UpdateDb("REPLACE INTO archive (id, boardid, timestamp, subject, length) VALUES ('%s', '%s', '%s', '%s', '%s')" % (thread['id'], board['id'], thread['timestamp'], _mysql.escape_string(thread['subject']), thread['length']))
+
+def throw_dice(dice):
+ qty = int(dice[0][1:])
+ if qty == 0:
+ raise UserError, "No tienes dados para lanzar."
+ if qty > 100:
+ qty = 100
+ sides = int(dice[1][1:]) if dice[1] else 6
+ if sides == 0:
+ raise UserError, "Tus dados no tienen caras."
+ if sides > 100:
+ sides = 100
+
+ string = "Lanzas "
+ string += "un dado de " if qty == 1 else (str(qty) + " dados de ")
+ string += "una cara." if sides == 1 else (str(sides) + " caras.")
+ string += " Resultado: <b>"
+
+ total = i = 0
+ while (i < qty):
+ total += random.randint(1,sides)
+ i += 1
+
+ string += str(total) + "</b>"
+
+ return string
+
+def magic_ball():
+ string = "La bola 8 mágica dice: <b>"
+ results = ["Sí.", "Es seguro.", "En mi opinión, sí.", "Está decidido que sí.", "Definitivamente sí.", "Es lo más probable.", "Buen pronóstico.", "Puedes confiar en ello.", "Todo apunta a que sí.", "Sin duda.", "Respuesta vaga, vuelve a intentarlo.", "Pregunta en otro momento.", "Mejor que no te lo diga ahora.", "No puedo predecirlo ahora.", "Concéntrate y vuelve a preguntar.", "No cuentes con ello.", "Mi respuesta es no.", "Mis fuentes me dicen que no.", "Pronóstico no muy bueno.", "Muy dudoso."]
+ string += random.choice(results) + "</b>"
+
+ return string
+
+def discord_hook(post, url):
+ import urllib2
+ import json
+
+ board = Settings._.BOARD
+
+ WEBHOOK_URL = "https://discordapp.com/api/webhooks/428025764974166018/msYu1-R3JRnG-cxrhAu3J7LbIPvzpBlJwbW5PFe5VEQaxVzjros9CXOpjZDahUE42Jgn"
+
+ data = {"content": "",
+ "ts": post['timestamp'],
+ "embeds": [{
+ "title": post['subject'],
+ "description": cut_home_msg(post['message'], 30),
+ "url": "https://bienvenidoainternet.org" + url,
+ "color": 11910504,
+ "timestamp": datetime.datetime.utcfromtimestamp(post['timestamp']).isoformat(),
+ "footer": { "text": board['name'] },
+ "thumbnail": { "url": "https://bienvenidoainternet.org/%s/thumb/%s" % (board['dir'], post['thumb']) },
+ "author": {
+ "name": "Nuevo hilo",
+ "icon_url": "https://bienvenidoainternet.org/0/junk/w/shobon.gif"
+ }}]
+ }
+ jsondata = json.dumps(data, separators=(',',':'))
+
+ opener = urllib2.build_opener()
+ opener.addheaders = [('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0')]
+ response = opener.open(WEBHOOK_URL, jsondata, 6)
+ the_page = response.read()
diff --git a/cgi/proxy.txt b/cgi/proxy.txt
new file mode 100644
index 0000000..5fc9460
--- /dev/null
+++ b/cgi/proxy.txt
@@ -0,0 +1,3251 @@
+190.151.94.45
+186.67.46.229
+109.107.112.139
+109.107.112.250
+109.107.97.45
+114.127.246.36
+114.224.167.170
+115.99.10.9
+116.17.92.221
+116.26.61.105
+116.48.224.179
+116.5.232.33
+116.72.165.208
+116.72.189.248
+117.102.81.138
+118.175.22.21
+118.220.175.207
+118.98.161.122
+118.98.165.42
+118.98.168.18
+119.115.52.109
+119.235.25.242
+119.62.128.38
+119.63.86.78
+119.70.40.101
+12.13.94.227
+12.208.118.96
+12.208.14.76
+12.208.168.97
+12.208.190.71
+12.240.37.195
+12.4.27.18
+12.47.164.114
+12.51.72.38
+12.72.169.156
+120.118.254.248
+120.125.80.133
+120.138.117.249
+120.138.80.226
+120.28.64.69
+121.108.31.23
+121.207.206.137
+121.207.42.244
+121.242.41.18
+121.245.199.202
+121.246.207.88
+121.52.147.45
+121.52.49.218
+121.58.193.10
+121.8.98.90
+121.9.230.162
+122.103.167.189
+122.103.185.182
+122.107.124.56
+122.116.65.95
+122.166.15.47
+122.17.74.118
+122.224.97.85
+122.6.245.14
+123.127.110.247
+123.131.44.66
+123.175.204.208
+123.233.121.164
+123.6.19.97
+124.115.177.53
+124.124.19.3
+124.129.174.114
+124.138.92.206
+124.195.158.155
+124.207.168.48
+124.217.198.4
+124.237.86.10
+124.30.233.111
+124.40.121.3
+124.53.159.169
+124.53.159.185
+124.81.224.174
+124.81.231.1
+125.135.15.181
+125.14.35.130
+125.160.244.159
+125.163.255.154
+125.240.55.130
+125.243.140.2
+125.243.149.130
+125.245.187.2
+125.245.196.194
+125.245.211.2
+125.248.165.178
+125.34.135.4
+125.34.30.201
+125.40.59.193
+125.46.24.134
+125.46.34.53
+125.46.95.230
+125.65.113.61
+129.22.90.248
+129.70.142.34
+129.93.193.140
+130.63.177.192
+131.196.115.10
+131.196.13.5
+134.39.27.37
+138.0.152.162
+138.0.231.66
+138.117.109.154
+138.117.143.226
+138.117.149.68
+138.117.84.90
+138.122.191.224
+138.186.176.175
+138.186.178.161
+138.186.179.219
+138.186.201.214
+138.36.98.107
+138.59.176.244
+138.59.176.246
+138.59.176.67
+140.113.169.49
+140.116.23.198
+140.117.178.234
+140.134.104.195
+140.134.208.93
+141.13.16.201
+141.13.16.202
+141.85.118.1
+141.85.254.118
+142.150.3.77
+142.59.52.201
+142.59.90.148
+143.255.142.99
+143.50.28.215
+146.101.147.140
+147.75.123.141
+147.75.123.142
+148.233.239.23
+148.233.239.24
+148.243.37.101
+148.244.102.98
+149.202.5.57
+151.11.232.92
+151.204.42.140
+151.79.46.58
+152.169.230.136
+152.204.130.62
+152.231.29.196
+152.231.74.252
+152.231.81.122
+156.17.93.77
+156.34.176.45
+157.182.52.224
+16.225.151.192
+160.7.251.98
+161.10.228.42
+161.132.100.69
+161.132.111.83
+161.132.176.28
+162.129.45.61
+163.17.151.126
+163.17.189.254
+163.24.133.117
+163.29.225.250
+163.30.113.254
+163.30.32.90
+164.77.134.13
+164.77.170.68
+164.78.252.25
+165.228.128.11
+165.228.132.11
+165.229.203.100
+168.196.112.250
+168.196.114.254
+168.234.75.142
+168.90.12.125
+168.90.12.126
+170.239.84.248
+170.239.86.37
+170.254.230.226
+170.78.46.53
+170.79.16.19
+170.84.126.164
+172.162.51.225
+172.163.102.206
+172.163.122.212
+172.163.195.118
+172.163.2.27
+172.164.11.216
+172.165.127.246
+172.166.67.103
+172.190.25.229
+172.192.219.161
+173.45.229.206
+174.0.50.242
+174.129.225.131
+174.142.104.57
+174.142.24.201
+174.142.36.90
+174.49.93.138
+176.56.122.156
+176.56.122.194
+176.56.98.135
+177.230.105.9
+177.234.12.202
+177.234.7.66
+177.250.223.82
+178.60.28.98
+179.42.186.120
+179.43.98.74
+179.49.114.110
+179.60.240.233
+179.60.241.246
+179.63.253.42
+181.10.176.50
+181.111.56.248
+181.112.138.198
+181.112.147.218
+181.112.147.90
+181.112.152.222
+181.112.188.90
+181.112.218.146
+181.112.219.187
+181.112.221.182
+181.112.221.2
+181.112.227.142
+181.112.228.126
+181.112.37.38
+181.112.39.186
+181.112.47.246
+181.112.51.174
+181.112.53.114
+181.112.55.6
+181.112.60.62
+181.112.61.146
+181.112.61.162
+181.112.61.178
+181.112.62.26
+181.113.116.134
+181.113.121.158
+181.113.123.58
+181.113.20.186
+181.113.28.150
+181.113.30.222
+181.113.5.142
+181.113.5.146
+181.113.5.186
+181.114.21.154
+181.115.241.90
+181.119.115.17
+181.129.1.154
+181.129.131.74
+181.129.2.186
+181.129.30.190
+181.129.39.42
+181.129.40.42
+181.129.51.106
+181.143.103.170
+181.143.123.114
+181.143.162.189
+181.143.186.82
+181.143.210.61
+181.143.221.226
+181.143.47.28
+181.143.65.117
+181.143.73.34
+181.143.9.34
+181.15.156.190
+181.167.159.174
+181.168.140.136
+181.168.148.151
+181.170.196.130
+181.174.79.53
+181.177.141.233
+181.177.242.82
+181.177.242.84
+181.188.130.14
+181.188.156.51
+181.188.199.152
+181.189.210.1
+181.189.235.11
+181.192.13.15
+181.196.145.106
+181.196.150.166
+181.196.178.106
+181.196.207.66
+181.196.27.162
+181.196.27.198
+181.196.28.34
+181.196.50.238
+181.196.57.54
+181.196.77.194
+181.199.178.12
+181.199.202.248
+181.211.101.10
+181.211.114.110
+181.211.13.126
+181.211.187.250
+181.211.191.228
+181.211.2.82
+181.211.3.190
+181.211.57.10
+181.211.59.22
+181.225.78.142
+181.225.79.164
+181.229.121.134
+181.29.113.251
+181.29.126.81
+181.39.17.138
+181.39.223.96
+181.39.28.228
+181.39.96.154
+181.40.115.186
+181.40.46.204
+181.40.93.118
+181.41.246.100
+181.44.24.56
+181.45.61.153
+181.47.9.239
+181.48.167.142
+181.48.216.38
+181.48.47.26
+181.49.121.21
+181.49.160.90
+181.49.200.90
+181.49.213.166
+181.49.41.206
+181.49.44.14
+181.49.50.226
+181.52.172.253
+181.55.148.39
+181.55.151.170
+181.55.188.184
+181.56.9.161
+181.57.156.186
+181.64.106.201
+181.64.233.120
+181.65.133.210
+181.66.51.109
+181.67.238.120
+181.74.133.166
+185.155.67.160
+185.177.74.179
+185.19.214.48
+185.65.186.192
+185.74.192.176
+185.98.232.119
+185.98.232.182
+186.0.195.194
+186.0.95.78
+186.1.180.214
+186.1.180.217
+186.1.183.102
+186.10.5.141
+186.10.5.142
+186.10.79.138
+186.101.136.10
+186.101.33.146
+186.103.154.101
+186.103.169.164
+186.103.169.166
+186.103.239.190
+186.117.128.92
+186.121.252.131
+186.147.161.185
+186.156.228.41
+186.156.236.98
+186.156.85.228
+186.178.10.78
+186.178.7.142
+186.18.225.108
+186.3.1.187
+186.38.79.28
+186.4.186.119
+186.4.192.202
+186.4.200.145
+186.42.161.30
+186.42.185.114
+186.42.188.110
+186.42.198.218
+186.42.97.154
+186.46.120.138
+186.46.128.102
+186.46.136.42
+186.46.138.14
+186.46.138.186
+186.46.151.22
+186.46.152.142
+186.46.153.174
+186.46.156.202
+186.46.163.102
+186.46.192.242
+186.46.232.34
+186.46.232.38
+186.46.250.14
+186.46.27.70
+186.46.28.14
+186.46.85.154
+186.46.88.226
+186.46.94.18
+186.46.94.54
+186.47.100.30
+186.47.102.58
+186.47.212.166
+186.47.23.118
+186.47.23.130
+186.47.232.250
+186.47.46.6
+186.47.72.198
+186.47.72.58
+186.47.96.90
+186.65.81.140
+186.67.90.12
+186.68.85.26
+186.68.93.46
+186.74.135.245
+186.74.190.82
+186.83.66.119
+186.88.21.87
+186.89.125.167
+186.89.227.238
+186.89.98.56
+186.91.185.237
+186.91.186.112
+186.91.222.58
+186.92.138.184
+186.92.144.24
+186.92.158.52
+186.92.167.175
+186.92.25.250
+186.92.85.128
+186.93.86.242
+186.95.18.58
+186.95.181.177
+186.95.220.5
+186.95.243.219
+186.95.57.249
+186.95.59.223
+187.11.250.36
+187.134.221.231
+187.141.79.55
+187.160.116.145
+187.160.149.88
+187.160.154.147
+187.160.245.156
+187.161.209.78
+187.161.228.47
+187.161.3.226
+187.188.182.116
+187.189.115.24
+187.189.26.10
+187.189.26.61
+187.189.47.196
+187.189.73.164
+187.189.75.110
+187.190.221.61
+187.190.64.28
+187.217.189.229
+187.237.138.186
+187.243.251.110
+187.243.251.30
+187.243.251.42
+187.246.183.227
+187.250.102.60
+187.49.217.2
+189.109.46.210
+189.111.166.103
+189.111.166.127
+189.122.171.234
+189.122.197.251
+189.126.103.252
+189.127.143.70
+189.14.68.130
+189.166.116.138
+189.19.10.23
+189.19.168.149
+189.19.60.123
+189.195.162.222
+189.196.15.9
+189.198.199.122
+189.198.239.74
+189.199.112.138
+189.20.207.150
+189.204.116.221
+189.204.219.149
+189.209.110.202
+189.211.209.44
+189.218.126.199
+189.218.127.200
+189.218.214.244
+189.218.68.175
+189.219.103.120
+189.219.229.50
+189.219.24.155
+189.219.241.16
+189.219.241.174
+189.219.76.118
+189.219.80.167
+189.219.88.243
+189.219.92.138
+189.219.99.41
+189.221.102.190
+189.23.208.37
+189.23.6.3
+189.26.245.173
+189.29.117.58
+189.29.27.185
+189.3.176.130
+189.37.28.147
+189.39.115.185
+189.47.129.62
+189.47.137.189
+189.53.181.9
+189.55.174.28
+189.55.219.176
+189.55.219.252
+189.56.61.33
+189.60.224.13
+189.79.63.28
+189.8.41.58
+190.0.22.10
+190.0.25.242
+190.0.35.6
+190.1.174.162
+190.101.137.157
+190.102.206.48
+190.103.31.61
+190.104.156.126
+190.104.179.138
+190.104.179.254
+190.104.195.210
+190.104.233.24
+190.104.69.9)
+190.104.71.30
+190.107.16.37
+190.108.192.24
+190.109.169.41
+190.11.115.133
+190.11.121.126
+190.11.26.58
+190.110.192.124
+190.111.233.208
+190.111.243.170
+190.112.108.29
+190.112.126.143
+190.112.40.138
+190.112.40.54
+190.112.41.116
+190.112.42.186
+190.116.175.2
+190.117.101.146
+190.117.115.150
+190.117.167.168
+190.117.167.239
+190.117.181.60
+190.12.102.205
+190.12.18.134
+190.12.48.158
+190.12.58.187
+190.12.63.166
+190.12.65.66
+190.121.158.114
+190.121.158.122
+190.121.167.218
+190.121.29.235
+190.122.20.85
+190.128.30.14
+190.129.1.141
+190.129.25.59
+190.13.174.19
+190.130.207.201
+190.131.203.90
+190.131.223.210
+190.131.235.134
+190.131.254.91
+190.138.249.182
+190.139.101.154
+190.139.49.20
+190.14.213.26
+190.14.233.90
+190.14.237.66
+190.14.245.142
+190.141.37.122
+190.142.221.66
+190.142.221.81
+190.145.30.188
+190.145.45.150
+190.145.80.114
+190.147.1.8
+190.147.100.219
+190.147.134.57
+190.147.160.240
+190.15.207.242
+190.15.213.137
+190.15.221.21
+190.15.222.24
+190.151.10.226
+190.151.68.116
+190.152.149.114
+190.152.150.62
+190.152.16.43
+190.152.18.182
+190.152.19.190
+190.152.37.114
+190.152.37.58
+190.152.4.54
+190.153.137.15
+190.153.142.191
+190.153.210.237
+190.163.22.64
+190.171.215.160
+190.18.134.27
+190.18.207.50
+190.180.62.246
+190.184.31.194
+190.186.111.59
+190.186.242.12
+190.186.46.14
+190.186.46.7
+190.186.5.54
+190.186.55.194
+190.186.58.170
+190.186.65.201
+190.186.7.114
+190.186.7.38
+190.196.168.46
+190.196.4.51
+190.198.30.0
+190.198.89.58
+190.199.213.29
+190.199.245.82
+190.200.63.90
+190.202.15.83
+190.203.229.8
+190.203.51.177
+190.204.0.202
+190.205.220.125
+190.205.45.146
+190.206.1.33
+190.206.143.42
+190.206.2.24
+190.207.254.204
+190.210.37.42
+190.210.99.221
+190.210.99.241
+190.211.80.154
+190.214.1.26
+190.214.10.54
+190.214.14.38
+190.214.31.230
+190.214.44.74
+190.214.56.138
+190.214.56.142
+190.215.253.251
+190.216.198.123
+190.217.3.162
+190.217.4.102
+190.217.55.10
+190.217.6.121
+190.219.217.140
+190.221.138.29
+190.225.18.133
+190.226.227.140
+190.228.202.235
+190.228.33.114
+190.228.38.78
+190.228.47.10
+190.23.175.185
+190.232.115.179
+190.232.168.242
+190.234.168.144
+190.234.181.250
+190.236.177.213
+190.236.188.8
+190.238.11.199
+190.238.68.190
+190.239.144.37
+190.239.148.244
+190.24.145.124
+190.242.119.68
+190.248.136.229
+190.249.160.169
+190.253.230.54
+190.254.148.213
+190.3.36.105
+190.33.255.26
+190.37.117.144
+190.38.40.46
+190.39.158.1
+190.39.195.72
+190.4.1.150
+190.4.30.195
+190.40.230.185
+190.42.124.87
+190.42.184.52
+190.42.34.95
+190.43.120.91
+190.43.40.153
+190.44.84.131
+190.49.46.80
+190.5.117.153
+190.5.122.174
+190.5.92.18
+190.52.168.58
+190.52.175.76
+190.53.89.103
+190.57.144.102
+190.57.144.42
+190.57.169.226
+190.57.189.78
+190.6.203.124
+190.6.29.173
+190.6.35.203
+190.60.215.172
+190.60.234.131
+190.60.251.44
+190.60.4.91
+190.63.154.197
+190.63.182.230
+190.63.187.230
+190.64.135.122
+190.64.160.101
+190.7.144.202
+190.7.149.53
+190.73.0.234
+190.73.55.61
+190.74.169.21
+190.75.198.179
+190.75.222.40
+190.78.165.211
+190.78.82.216
+190.79.140.223
+190.79.143.123
+190.79.91.74
+190.81.177.26
+190.82.117.18
+190.82.117.20
+190.85.122.134
+190.85.146.156
+190.9.56.170
+190.9.59.198
+190.9.60.10
+190.90.122.26
+190.90.193.212
+190.90.218.253
+190.93.179.126
+190.94.247.254
+190.95.19.236
+190.95.7.235
+190.96.47.237
+190.96.91.243
+191.102.122.3
+191.102.125.74
+191.102.125.75
+191.102.84.196
+191.102.89.54
+191.103.252.169
+191.103.253.25
+191.103.253.89
+191.98.183.138
+191.98.183.139
+192.115.104.88
+192.116.226.69
+193.111.120.47
+193.153.38.221
+193.171.32.6
+193.173.119.83
+193.188.95.146
+193.194.69.155
+193.220.32.246
+193.251.1.244
+193.251.35.18
+193.30.164.3
+193.40.59.83
+193.52.195.6
+193.69.186.83
+193.86.86.86
+194.117.157.72
+194.149.220.21
+194.149.222.33
+194.176.176.82
+194.177.202.247
+194.30.228.83
+194.46.229.3
+194.55.138.53
+194.6.1.219
+194.79.113.83
+194.9.85.141
+194.90.179.13
+195.103.8.10
+195.135.236.215
+195.146.78.214
+195.160.188.163
+195.167.64.193
+195.209.224.91
+195.242.192.18
+195.246.155.219
+195.47.14.193
+195.54.22.74
+195.55.85.254
+195.76.242.227
+195.89.143.211
+195.97.171.76
+196.12.145.125
+196.20.7.74
+196.202.252.244
+196.203.172.166
+196.23.147.34
+196.23.52.170
+196.25.52.36
+196.27.116.60
+196.38.62.210
+196.40.43.34
+198.164.83.28
+198.49.131.250
+198.83.124.250
+199.193.10.202
+199.193.13.202
+199.216.215.21
+199.253.99.202
+2.136.85.132
+20.132.16.22
+200.101.13.202
+200.101.92.148
+200.102.191.228
+200.102.217.207
+200.104.104.91
+200.104.250.92
+200.105.148.74
+200.105.227.182
+200.107.29.70
+200.109.108.137
+200.109.119.126
+200.109.228.66
+200.109.72.53
+200.11.138.149
+200.110.13.169
+200.111.102.66
+200.111.121.18
+200.111.121.19
+200.111.121.21
+200.111.122.107
+200.112.216.5
+200.112.228.211
+200.112.70.53
+200.112.84.5
+200.114.104.4
+200.114.97.14
+200.115.25.118
+200.115.25.119
+200.115.25.120
+200.116.132.2
+200.116.209.58
+200.116.227.138
+200.116.227.99
+200.116.69.6
+200.117.240.56
+200.118.104.79
+200.118.168.237
+200.119.239.252
+200.119.56.48
+200.120.195.140
+200.120.224.207
+200.122.211.14
+200.123.102.149
+200.123.254.177
+200.123.50.43
+200.123.55.253
+200.125.202.198
+200.128.6.235
+200.135.246.2
+200.139.78.114
+200.14.126.36
+200.140.171.24
+200.142.99.166
+200.146.85.16
+200.148.230.217
+200.150.139.211
+200.152.107.56
+200.158.26.223
+200.159.216.247
+200.16.208.187
+200.160.244.246
+200.160.96.39
+200.161.108.72
+200.161.118.198
+200.161.125.6
+200.161.31.11
+200.161.81.98
+200.162.113.32
+200.165.140.104
+200.167.145.129
+200.167.86.209
+200.168.152.152
+200.171.17.23
+200.171.175.157
+200.171.232.140
+200.174.85.193
+200.174.85.195
+200.187.136.122
+200.192.210.98
+200.195.95.38
+200.2.125.90
+200.202.214.4
+200.204.121.196
+200.204.154.29
+200.204.235.123
+200.205.87.106
+200.206.46.178
+200.207.126.232
+200.207.48.252
+200.207.9.168
+200.209.175.243
+200.21.225.82
+200.21.24.79
+200.217.16.202
+200.217.53.234
+200.217.76.37
+200.221.10.104
+200.225.0.174
+200.232.184.253
+200.232.94.34
+200.233.77.95
+200.242.95.171
+200.245.35.131
+200.25.250.233
+200.252.201.144
+200.27.164.196
+200.29.191.151
+200.3.207.39
+200.30.101.2
+200.31.137.58
+200.31.42.3
+200.35.51.206
+200.37.231.66
+200.37.80.11
+200.4.253.27
+200.40.113.134
+200.41.230.102
+200.41.230.105
+200.42.225.106
+200.42.45.211
+200.43.138.196
+200.43.187.129
+200.44.217.52
+200.45.22.134
+200.45.32.150
+200.46.109.82
+200.48.106.34
+200.48.129.123
+200.49.181.162
+200.5.83.150
+200.50.170.38
+200.52.4.82
+200.54.103.76
+200.54.108.54
+200.54.194.12
+200.54.212.234
+200.55.208.203
+200.55.219.178
+200.59.9.18
+200.6.180.2
+200.6.225.122
+200.60.16.22
+200.61.19.208
+200.61.6.50
+200.65.129.1
+200.65.129.2
+200.67.117.246
+200.67.85.1
+200.68.34.99
+200.68.97.116
+200.69.245.33
+200.7.252.182
+200.72.187.74
+200.72.187.75
+200.74.158.86
+200.75.12.213
+200.75.9.35
+200.80.230.10
+200.81.172.98
+200.82.157.103
+200.83.4.60
+200.84.128.104
+200.84.138.166
+200.84.14.108
+200.84.150.113
+200.84.151.133
+200.84.181.210
+200.84.47.228
+200.85.123.154
+200.85.37.254
+200.85.59.250
+200.87.180.226
+200.87.43.50
+200.88.223.99
+200.89.129.99
+200.90.148.195
+200.93.43.43
+200.93.82.36
+200.94.92.230
+200.95.239.254
+200.96.193.100
+201.0.17.76
+201.1.113.10
+201.1.63.111
+201.10.42.166
+201.100.42.12
+201.116.199.243
+201.116.70.1
+201.12.116.112
+201.122.180.91
+201.13.169.167
+201.13.176.9
+201.13.187.229
+201.132.155.10
+201.132.155.70
+201.132.160.210
+201.132.162.254
+201.14.225.222
+201.144.14.229
+201.148.23.69
+201.15.143.25
+201.15.218.158
+201.15.30.1
+201.155.194.182
+201.16.232.37
+201.160.37.2
+201.165.55.14
+201.166.150.158
+201.166.23.226
+201.167.56.18
+201.17.163.70
+201.17.188.5
+201.172.123.9
+201.172.139.205
+201.172.194.92
+201.172.80.223
+201.173.158.27
+201.173.223.180
+201.173.240.145
+201.183.235.77
+201.184.224.98
+201.184.227.178
+201.184.237.194
+201.184.244.186
+201.184.250.76
+201.184.252.42
+201.184.72.34
+201.184.81.178
+201.184.86.218
+201.187.110.174
+201.190.181.76
+201.20.89.10
+201.208.117.43
+201.208.133.140
+201.208.155.57
+201.208.38.95
+201.208.43.80
+201.209.12.56
+201.209.173.54
+201.211.154.15
+201.211.189.154
+201.213.54.67
+201.216.213.201
+201.217.217.26
+201.217.245.108
+201.217.246.34
+201.219.184.227
+201.219.218.17
+201.219.218.18
+201.220.84.146
+201.222.52.30
+201.222.55.93
+201.222.99.12
+201.228.89.170
+201.229.208.2
+201.229.208.3
+201.238.239.88
+201.24.125.218
+201.240.212.1
+201.240.52.170
+201.242.120.234
+201.242.192.120
+201.243.173.251
+201.243.63.12
+201.245.190.38
+201.246.116.96
+201.249.52.100
+201.249.88.226
+201.251.126.164
+201.252.106.88
+201.252.14.124
+201.252.211.201
+201.253.124.65
+201.253.144.1
+201.253.9.185
+201.255.141.247
+201.255.178.224
+201.26.133.204
+201.26.169.10
+201.26.19.180
+201.26.200.105
+201.26.212.10
+201.26.8.186
+201.27.2.220
+201.3.184.222
+201.31.247.225
+201.39.29.50
+201.42.69.73
+201.43.31.164
+201.44.24.98
+201.45.188.169
+201.53.148.108
+201.53.73.44
+201.59.184.124
+201.66.27.218
+201.68.18.124
+201.68.227.8
+201.68.244.150
+201.68.77.129
+201.73.45.70
+201.73.73.130
+201.74.205.134
+201.75.20.103
+201.75.78.76
+201.76.141.210
+201.76.29.82
+201.80.187.222
+201.80.207.132
+201.86.70.162
+201.88.248.243
+201.93.128.110
+202.102.73.145
+202.104.189.20
+202.104.20.181
+202.105.138.19
+202.105.182.12
+202.105.182.13
+202.105.182.15
+202.105.182.16
+202.105.182.20
+202.105.182.33
+202.105.182.87
+202.105.230.226
+202.106.139.88
+202.108.122.38
+202.110.204.18
+202.114.66.170
+202.115.202.250
+202.12.73.20
+202.129.181.242
+202.134.202.251
+202.141.25.92
+202.143.140.250
+202.143.154.242
+202.145.3.101
+202.147.168.58
+202.153.41.102
+202.164.191.186
+202.168.193.131
+202.177.119.4
+202.188.222.2
+202.194.133.31
+202.194.202.7
+202.239.243.116
+202.29.137.147
+202.3.217.125
+202.39.6.27
+202.41.105.162
+202.41.181.24
+202.44.8.100
+202.54.169.233
+202.54.217.164
+202.54.61.99
+202.58.163.201
+202.58.86.8
+202.63.177.3
+202.65.43.172
+202.70.36.242
+202.70.88.138
+202.71.240.33
+202.78.225.1
+202.78.227.32
+202.80.127.29
+202.87.179.206
+202.9.136.40
+202.94.203.89
+202.94.212.199
+202.95.169.175
+202.98.141.200
+202.98.23.114
+202.98.23.116
+202.99.225.45
+202.99.29.27
+203.101.61.76
+203.110.240.22
+203.113.137.66
+203.113.34.239
+203.117.67.122
+203.123.240.112
+203.124.21.224
+203.129.53.177
+203.130.203.41
+203.149.32.30
+203.151.40.4
+203.155.16.130
+203.158.167.152
+203.160.1.103
+203.160.1.112
+203.160.1.121
+203.160.1.130
+203.160.1.66
+203.160.1.75
+203.160.1.85
+203.160.1.94
+203.162.183.222
+203.186.92.119
+203.193.138.148
+203.196.67.107
+203.197.37.46
+203.200.75.165
+203.202.203.8
+203.67.172.29
+203.69.244.194
+203.69.39.251
+203.76.185.189
+203.78.11.73
+203.82.52.210
+203.86.31.92
+204.111.219.182
+204.196.104.27
+204.85.72.128
+205.138.18.80
+205.200.37.111
+206.174.3.131
+206.230.106.206
+206.49.33.250
+206.51.224.46
+207.102.0.15
+207.161.20.188
+207.167.236.137
+207.181.207.36
+207.182.152.72
+207.192.207.240
+207.192.209.237
+207.248.102.18
+207.248.230.70
+207.249.163.139
+207.38.251.111
+207.50.148.37
+207.61.241.100
+207.61.38.67
+207.99.23.198
+208.107.124.142
+208.107.18.112
+208.110.73.34
+208.34.14.113
+208.34.14.165
+208.53.196.161
+208.53.199.48
+208.53.199.75
+208.62.125.146
+208.66.171.217
+208.77.219.76
+208.96.122.142
+208.96.133.198
+208.96.213.149
+208.98.17.40
+209.1.163.63
+209.124.242.193
+209.137.150.138
+209.145.114.173
+209.159.184.219
+209.159.204.250
+209.159.228.202
+209.159.229.219
+209.159.241.112
+209.17.186.25
+209.195.4.27
+209.211.7.12
+209.218.218.171
+209.4.229.39
+209.45.108.175
+209.45.48.205
+209.47.38.116
+209.79.65.6
+209.89.66.6
+21.11.27.110
+210.12.86.181
+210.163.167.162
+210.192.111.173
+210.204.118.194
+210.21.12.94
+210.219.227.52
+210.23.124.178
+210.240.54.8
+210.245.63.218
+210.245.80.10
+210.245.80.15
+210.254.8.52
+210.34.14.166
+210.4.10.134
+210.51.4.173
+210.52.15.210
+210.74.254.35
+210.8.92.2
+210.82.40.243
+210.86.181.202
+210.92.128.194
+210.96.65.4
+211.108.62.230
+211.114.116.60
+211.115.185.41
+211.115.185.42
+211.115.185.44
+211.138.198.6
+211.139.120.69
+211.140.138.39
+211.140.151.214
+211.140.192.186
+211.15.62.123
+211.161.197.182
+211.21.111.227
+211.233.21.166
+211.45.21.165
+211.76.175.5
+211.90.114.199
+211.90.22.106
+211.93.108.113
+211.99.188.218
+212.102.0.104
+212.116.219.202
+212.117.63.145
+212.119.69.187
+212.12.157.130
+212.123.91.61
+212.138.84.62
+212.15.44.9
+212.165.156.74
+212.17.86.109
+212.179.127.188
+212.225.233.154
+212.231.65.8
+212.24.238.155
+212.243.183.5
+212.38.100.62
+212.4.98.158
+212.44.61.185
+212.60.65.206
+212.72.120.135
+212.76.90.2
+212.93.193.82
+213.137.130.166
+213.154.216.55
+213.158.112.202
+213.16.133.130
+213.16.20.140
+213.171.255.2
+213.174.145.1
+213.174.145.193
+213.174.145.250
+213.180.131.135
+213.185.116.152
+213.186.116.57
+213.212.75.12
+213.231.139.130
+213.249.237.196
+213.25.170.98
+213.25.29.12
+213.250.162.237
+213.255.229.205
+213.27.152.15
+213.27.71.250
+213.4.106.85
+213.4.106.86
+213.55.87.105
+213.55.87.205
+213.82.91.94
+213.97.52.28
+216.114.194.18
+216.117.225.240
+216.119.183.110
+216.127.32.22
+216.168.43.11
+216.19.216.44
+216.217.98.100
+216.228.57.247
+216.23.162.169
+216.230.72.76
+216.241.14.94
+216.241.36.82
+216.36.141.205
+216.72.196.21
+216.72.63.198
+216.80.118.13
+216.93.253.150
+217.10.246.2
+217.10.246.4
+217.117.136.88
+217.126.5.224
+217.147.30.24
+217.153.114.66
+217.174.98.198
+217.218.243.61
+217.219.211.89
+217.31.51.76
+217.31.51.77
+217.70.56.161
+218.104.180.228
+218.118.100.86
+218.127.146.43
+218.14.227.197
+218.14.227.198
+218.15.63.46
+218.202.48.81
+218.206.194.247
+218.229.29.128
+218.241.81.219
+218.248.20.160
+218.248.31.212
+218.249.83.87
+218.252.37.227
+218.26.204.66
+218.28.46.250
+218.28.58.86
+218.5.133.146
+218.50.52.210
+218.56.32.230
+218.56.64.210
+218.56.64.211
+218.56.64.212
+218.56.64.213
+218.58.136.14
+218.64.88.30
+218.7.48.22
+218.70.172.2
+218.75.100.114
+218.75.25.137
+218.75.76.74
+218.75.83.98
+218.76.207.31
+218.9.114.85
+218.94.9.38
+218.97.194.94
+219.113.113.52
+219.191.64.95
+219.208.194.33
+219.22.30.44
+219.240.36.173
+219.240.36.175
+219.25.212.32
+219.34.2.65
+219.37.208.150
+219.43.150.92
+219.43.228.154
+219.58.72.191
+219.64.91.118
+220.128.189.35
+220.130.81.188
+220.173.139.172
+220.194.55.160
+220.225.225.148
+220.227.138.82
+220.227.47.2
+220.227.47.20
+220.227.47.6
+220.231.29.99
+220.33.204.136
+220.5.108.156
+220.53.245.8
+220.56.96.67
+220.58.24.40
+220.6.120.99
+220.70.2.137
+221.11.27.110
+221.120.196.138
+221.122.66.84
+221.130.177.147
+221.139.50.83
+221.16.4.11
+221.178.141.159
+221.192.132.194
+221.2.216.38
+221.202.118.17
+221.224.95.158
+221.233.134.87
+221.3.2.61
+221.6.62.90
+221.8.74.211
+222.124.172.220
+222.191.242.225
+222.221.6.144
+222.240.208.14
+222.247.62.195
+222.35.90.16
+222.68.206.11
+222.68.207.11
+222.73.205.27
+222.83.228.17
+222.83.228.34
+222.86.132.20
+222.92.116.39
+24.10.186.31
+24.10.84.226
+24.100.18.152
+24.108.129.42
+24.108.35.246
+24.11.124.76
+24.11.20.98
+24.11.68.191
+24.11.90.41
+24.111.38.223
+24.116.11.52
+24.116.206.33
+24.118.147.89
+24.118.240.6
+24.119.169.151
+24.119.54.123
+24.12.214.237
+24.12.3.143
+24.125.125.26
+24.125.155.7
+24.125.158.91
+24.125.244.207
+24.125.73.200
+24.125.77.116
+24.127.113.227
+24.13.108.167
+24.131.171.96
+24.136.244.198
+24.137.215.227
+24.139.68.242
+24.14.107.77
+24.14.112.139
+24.140.13.237
+24.143.226.7
+24.151.126.249
+24.153.249.240
+24.154.129.8
+24.156.135.87
+24.16.192.63
+24.161.131.67
+24.167.83.34
+24.168.26.236
+24.170.37.75
+24.170.82.144
+24.170.90.111
+24.174.246.62
+24.175.116.55
+24.175.131.67
+24.175.136.94
+24.175.147.159
+24.182.38.24
+24.185.121.80
+24.185.21.20
+24.188.121.167
+24.188.125.225
+24.188.251.54
+24.189.5.235
+24.190.104.34
+24.190.214.200
+24.192.240.240
+24.193.87.7
+24.197.130.9
+24.2.24.6
+24.2.69.26
+24.20.45.101
+24.205.202.45
+24.208.222.208
+24.208.37.143
+24.210.148.161
+24.211.221.167
+24.211.49.0
+24.215.67.225
+24.215.89.254
+24.217.148.83
+24.217.194.73
+24.22.86.147
+24.228.49.186
+24.23.182.99
+24.23.199.14
+24.23.29.41
+24.230.163.136
+24.230.181.207
+24.230.182.225
+24.230.46.187
+24.237.24.27
+24.238.20.152
+24.242.236.98
+24.250.66.218
+24.254.113.238
+24.254.28.207
+24.254.34.183
+24.255.169.250
+24.255.247.79
+24.3.105.116
+24.3.253.43
+24.30.58.77
+24.30.62.26
+24.30.90.20
+24.4.239.144
+24.44.199.190
+24.44.219.167
+24.44.95.243
+24.45.174.137
+24.46.74.16
+24.47.247.186
+24.59.34.24
+24.61.52.46
+24.67.14.108
+24.7.230.76
+24.70.39.70
+24.77.207.136
+24.77.22.225
+24.78.169.73
+24.78.186.206
+24.8.191.246
+24.83.40.206
+24.83.69.55
+24.85.205.35
+24.89.198.236
+24.89.218.113
+24.9.22.230
+24.90.184.242
+24.98.12.137
+24.98.204.26
+24.98.81.111
+24.98.99.4
+31.25.181.98
+37.46.158.141
+37.46.158.199
+38.123.201.17
+41.205.107.65
+41.210.252.11
+41.211.224.66
+41.211.232.39
+41.221.177.29
+45.4.88.66
+45.7.132.58
+46.37.120.155
+5.154.37.28
+5.40.117.250
+5.40.117.253
+5.61.212.42
+58.137.8.85
+58.17.3.2
+58.181.22.190
+58.221.41.86
+58.222.254.13
+58.29.56.2
+58.56.108.114
+58.68.51.46
+58.69.190.50
+58.8.150.146
+58.83.197.27
+59.120.104.60
+59.144.175.48
+59.165.1.214
+59.177.176.21
+59.36.98.154
+59.39.145.178
+59.42.250.145
+59.45.207.24
+59.56.174.199
+59.92.21.80
+59.92.3.208
+59.94.177.245
+59.94.38.39
+59.94.41.39
+59.95.205.216
+60.12.227.209
+60.191.246.17
+60.191.96.90
+60.213.185.214
+60.218.99.18
+60.247.2.241
+60.250.139.213
+60.250.27.242
+60.28.182.205
+60.28.209.8
+60.32.115.204
+60.49.216.82
+60.49.51.63
+60.5.108.142
+61.12.149.73
+61.120.148.32
+61.131.48.219
+61.133.196.36
+61.133.196.40
+61.135.158.109
+61.135.158.125
+61.135.158.129
+61.135.158.130
+61.138.130.229
+61.139.73.6
+61.142.169.98
+61.142.81.37
+61.144.109.96
+61.152.154.19
+61.153.140.106
+61.156.42.123
+61.159.214.215
+61.159.235.36
+61.166.68.71
+61.166.68.72
+61.167.117.83
+61.17.232.227
+61.172.246.180
+61.172.249.94
+61.172.249.96
+61.175.139.6
+61.175.226.78
+61.178.128.208
+61.180.73.66
+61.182.66.53
+61.187.187.28
+61.19.158.212
+61.19.78.44
+61.234.254.69
+61.236.87.104
+61.238.104.200
+61.244.119.198
+61.27.47.49
+61.32.11.130
+61.54.82.130
+61.6.163.30
+61.60.55.220
+61.8.73.59
+61.91.242.19
+62.103.76.22
+62.119.28.242
+62.15.205.71
+62.159.143.172
+62.164.251.180
+62.168.41.61
+62.192.132.126
+62.193.246.10
+62.215.195.85
+62.49.143.76
+62.76.32.166
+62.81.76.18
+62.90.57.24
+62.99.77.116
+63.119.14.20
+64.111.32.54
+64.114.203.21
+64.17.66.234
+64.179.170.189
+64.184.203.60
+64.203.49.117
+64.207.224.70
+64.30.123.252
+64.58.28.250
+64.66.192.61
+64.79.197.36
+64.79.199.35
+64.79.209.203
+64.83.209.110
+64.86.28.118
+64.94.19.10
+65.190.207.153
+65.191.10.80
+65.255.71.17
+65.255.74.9
+65.28.103.187
+65.28.107.26
+65.28.109.2
+65.28.8.13
+65.29.182.114
+65.29.85.76
+65.30.216.140
+65.30.92.48
+65.35.247.52
+65.7.5.33
+65.75.189.33
+65.87.170.39
+66.128.83.112
+66.131.89.59
+66.135.163.52
+66.158.95.37
+66.165.197.13
+66.166.1.181
+66.167.100.59
+66.167.228.62
+66.168.71.215
+66.177.219.202
+66.178.127.24
+66.197.167.90
+66.198.41.11
+66.199.247.42
+66.214.17.189
+66.214.213.172
+66.219.59.95
+66.225.182.65
+66.229.205.251
+66.233.165.31
+66.234.204.35
+66.244.214.230
+66.244.214.249
+66.25.114.65
+66.253.168.169
+66.29.36.95
+66.38.121.88
+66.41.14.103
+66.41.150.79
+66.41.189.96
+66.56.175.206
+66.56.183.16
+66.57.1.142
+66.57.230.14
+66.57.75.68
+66.67.106.227
+66.90.237.241
+66.91.101.52
+67.100.121.150
+67.149.165.201
+67.149.215.109
+67.163.161.226
+67.168.222.227
+67.182.204.248
+67.184.38.23
+67.188.156.177
+67.19.148.234
+67.191.141.209
+67.191.220.137
+67.201.77.7
+67.48.16.231
+67.48.22.73
+67.48.23.137
+67.49.150.210
+67.49.162.117
+67.60.113.96
+67.65.242.94
+67.69.254.240
+67.69.254.241
+67.69.254.242
+67.69.254.244
+67.69.254.245
+67.69.254.246
+67.69.254.247
+67.69.254.248
+67.69.254.249
+67.69.254.250
+67.69.254.251
+67.69.254.252
+67.69.254.254
+67.69.254.255
+67.80.47.215
+67.80.81.65
+67.81.185.139
+67.81.225.132
+67.81.235.37
+67.82.22.33
+67.84.115.34
+67.84.148.144
+67.84.35.131
+67.84.84.89
+67.86.193.156
+67.87.64.23
+67.9.1.214
+67.9.20.215
+67.9.23.8
+67.9.25.132
+67.9.255.2
+67.9.28.224
+67.9.5.59
+67.9.5.92
+68.1.131.15
+68.10.87.155
+68.100.213.42
+68.102.90.174
+68.104.55.221
+68.105.0.173
+68.105.12.164
+68.108.224.145
+68.108.23.74
+68.109.170.127
+68.11.145.150
+68.11.181.113
+68.11.181.82
+68.11.182.166
+68.11.226.141
+68.11.237.184
+68.11.249.230
+68.111.127.37
+68.111.158.24
+68.111.231.178
+68.111.57.10
+68.113.102.37
+68.114.149.164
+68.115.77.233
+68.117.211.122
+68.118.245.35
+68.12.148.195
+68.12.169.13
+68.13.220.63
+68.144.70.254
+68.148.10.200
+68.148.86.171
+68.149.66.202
+68.173.44.218
+68.184.185.242
+68.184.56.83
+68.192.166.141
+68.198.151.89
+68.198.72.147
+68.199.107.24
+68.199.140.101
+68.2.143.105
+68.201.24.46
+68.201.46.56
+68.205.170.214
+68.207.186.253
+68.207.187.211
+68.224.71.23
+68.227.129.204
+68.228.236.251
+68.229.158.96
+68.36.188.103
+68.42.247.66
+68.44.106.173
+68.45.42.160
+68.51.214.245
+68.52.31.249
+68.54.70.19
+68.55.215.207
+68.55.225.102
+68.59.217.62
+68.59.44.32
+68.60.168.230
+68.60.188.125
+68.60.189.199
+68.62.176.8
+68.62.218.171
+68.62.240.77
+68.63.48.36
+68.8.224.217
+68.8.227.134
+68.81.191.233
+68.83.114.81
+68.83.156.112
+68.84.126.225
+68.84.47.147
+68.9.156.171
+68.9.242.26
+68.97.121.200
+68.97.127.248
+68.97.206.212
+68.98.0.233
+69.113.232.218
+69.114.237.205
+69.114.82.211
+69.115.86.239
+69.116.109.16
+69.116.42.119
+69.118.237.19
+69.119.206.230
+69.122.153.0
+69.122.222.90
+69.123.43.126
+69.123.44.118
+69.126.24.134
+69.127.102.247
+69.127.115.255
+69.127.175.231
+69.136.136.125
+69.136.58.38
+69.136.70.21
+69.138.220.71
+69.138.46.194
+69.139.124.172
+69.139.167.203
+69.142.108.83
+69.142.201.72
+69.151.73.128
+69.154.135.199
+69.161.78.160
+69.180.245.32
+69.180.8.201
+69.181.89.167
+69.212.49.215
+69.224.140.241
+69.23.105.226
+69.23.122.70
+69.242.176.42
+69.242.220.173
+69.245.152.207
+69.245.52.76
+69.246.117.136
+69.246.123.195
+69.246.123.26
+69.246.16.28
+69.246.218.125
+69.246.45.182
+69.246.61.14
+69.249.151.19
+69.250.19.191
+69.250.8.55
+69.253.188.82
+69.253.79.204
+69.254.246.123
+69.255.105.108
+69.46.16.232
+69.47.174.178
+69.62.141.46
+69.64.38.125
+69.65.42.44
+69.7.105.30
+69.71.85.202
+69.71.95.69
+69.81.17.45
+69.86.12.190
+69.86.90.176
+69.88.35.197
+69.92.244.253
+69.92.76.23
+70.117.243.108
+70.118.212.66
+70.119.146.106
+70.123.89.57
+70.125.110.220
+70.127.205.107
+70.161.20.242
+70.161.72.245
+70.161.93.238
+70.172.242.76
+70.176.119.94
+70.176.124.127
+70.176.191.105
+70.177.51.135
+70.177.53.179
+70.177.61.32
+70.180.206.70
+70.180.62.153
+70.186.168.130
+70.186.174.186
+70.225.202.96
+70.238.144.197
+70.238.47.146
+70.44.209.4
+70.45.34.75
+70.64.225.85
+70.64.250.176
+70.64.252.25
+70.66.218.123
+70.67.105.211
+70.72.152.13
+70.74.213.38
+70.76.83.81
+70.78.22.121
+70.82.140.29
+70.86.138.210
+70.89.83.173
+70.95.110.195
+71.10.72.221
+71.11.157.62
+71.14.40.151
+71.14.94.250
+71.14.95.198
+71.178.155.167
+71.192.111.90
+71.192.233.196
+71.192.234.31
+71.194.0.41
+71.194.217.243
+71.197.189.88
+71.200.233.55
+71.201.60.149
+71.203.142.40
+71.203.162.68
+71.203.213.53
+71.205.102.196
+71.205.107.223
+71.205.109.70
+71.205.111.45
+71.205.113.223
+71.205.123.131
+71.205.238.140
+71.205.238.236
+71.205.37.198
+71.206.111.22
+71.207.56.148
+71.224.107.188
+71.224.87.71
+71.225.31.105
+71.229.16.100
+71.233.34.93
+71.235.86.254
+71.237.41.13
+71.237.98.13
+71.238.39.207
+71.239.246.54
+71.250.251.214
+71.252.182.73
+71.41.99.190
+71.57.11.142
+71.7.246.230
+71.8.7.200
+71.8.98.36
+71.80.99.54
+71.82.77.13
+71.82.9.16
+71.85.121.118
+71.86.144.144
+71.86.150.78
+71.86.152.122
+71.89.55.232
+71.90.230.116
+72.128.40.214
+72.135.0.206
+72.137.122.212
+72.140.93.121
+72.141.35.81
+72.172.203.25
+72.174.123.174
+72.174.161.51
+72.175.137.173
+72.178.207.128
+72.178.248.236
+72.190.118.76
+72.190.122.130
+72.196.135.11
+72.197.212.200
+72.200.193.225
+72.203.130.111
+72.207.200.241
+72.207.59.75
+72.213.190.99
+72.213.194.229
+72.216.12.173
+72.219.39.216
+72.222.172.142
+72.222.180.102
+72.227.236.241
+72.227.36.24
+72.229.126.142
+72.24.129.249
+72.24.145.190
+72.24.212.232
+72.39.117.31
+72.4.82.241
+72.52.96.51
+72.9.82.131
+74.105.84.51
+74.129.180.10
+74.131.139.186
+74.137.109.66
+74.141.111.159
+74.15.86.86
+74.171.2.153
+74.174.5.68
+74.193.33.187
+74.194.57.223
+74.194.61.154
+74.196.148.50
+74.197.219.75
+74.210.84.24
+74.222.1.99
+74.223.183.66
+74.54.156.73
+74.55.86.131
+74.56.176.187
+74.71.197.158
+74.73.99.82
+74.77.117.65
+75.109.46.197
+75.136.135.2
+75.153.242.91
+75.179.140.16
+75.180.26.184
+75.183.7.150
+75.184.41.3
+75.34.137.36
+75.64.250.79
+75.64.35.123
+75.65.64.163
+75.66.105.218
+75.71.42.32
+75.74.84.122
+75.81.22.134
+75.83.57.219
+75.85.136.141
+75.87.150.14
+75.87.189.110
+75.93.212.146
+75.94.80.132
+76.100.211.252
+76.102.95.54
+76.105.105.96
+76.106.127.211
+76.107.108.144
+76.107.116.228
+76.107.117.227
+76.107.125.246
+76.107.136.73
+76.107.136.74
+76.107.137.6
+76.107.142.104
+76.107.151.18
+76.107.156.235
+76.107.208.13
+76.107.38.217
+76.107.42.95
+76.107.44.181
+76.107.93.147
+76.109.161.247
+76.11.23.85
+76.110.119.179
+76.110.138.122
+76.110.211.162
+76.112.137.136
+76.112.150.1
+76.112.25.186
+76.113.8.160
+76.115.37.7
+76.116.82.97
+76.117.113.157
+76.117.201.40
+76.122.108.158
+76.123.128.94
+76.123.18.157
+76.124.36.104
+76.127.22.84
+76.160.138.68
+76.17.104.79
+76.17.69.212
+76.17.88.209
+76.170.85.232
+76.173.155.23
+76.173.95.124
+76.176.208.180
+76.178.184.139
+76.182.57.113
+76.183.112.143
+76.187.117.138
+76.20.228.53
+76.214.55.31
+76.22.0.234
+76.22.128.2
+76.244.189.250
+76.247.168.177
+76.25.236.65
+76.27.54.31
+76.28.1.186
+76.28.208.70
+76.28.250.36
+76.29.10.61
+76.29.243.55
+76.69.32.139
+76.85.159.130
+76.88.67.220
+76.89.19.252
+76.89.23.238
+76.9.42.163
+76.94.48.145
+76.98.47.148
+77.100.110.112
+77.100.241.213
+77.101.103.239
+77.101.103.91
+77.101.58.117
+77.102.121.34
+77.102.178.145
+77.102.220.82
+77.102.25.197
+77.103.130.91
+77.103.153.29
+77.103.254.43
+77.103.84.134
+77.226.237.178
+77.226.240.50
+77.237.91.60
+77.240.82.6
+77.242.169.70
+77.242.233.44
+77.242.33.5
+77.244.218.34
+77.41.140.70
+77.71.0.158
+77.78.1.119
+77.88.66.251
+77.96.105.84
+77.96.143.223
+77.97.103.232
+77.97.84.28
+77.98.146.168
+77.98.146.183
+77.99.11.82
+77.99.113.100
+77.99.162.166
+77.99.183.136
+77.99.30.244
+78.109.149.162
+78.129.239.35
+78.138.131.150
+78.154.132.241
+78.162.45.2
+78.224.128.22
+78.24.49.96
+78.31.64.74
+78.38.244.2
+78.38.244.9
+78.43.175.129
+79.152.2.193
+79.156.24.91
+80.0.76.158
+80.105.84.250
+80.127.3.115
+80.143.220.5
+80.150.14.123
+80.153.156.21
+80.192.214.147
+80.192.55.191
+80.192.75.52
+80.193.155.208
+80.193.158.197
+80.193.189.226
+80.193.72.145
+80.195.248.30
+80.195.3.136
+80.195.53.253
+80.227.1.101
+80.237.140.233
+80.237.38.77
+80.247.71.56
+80.25.120.104
+80.26.178.158
+80.34.164.229
+80.36.35.109
+80.4.59.69
+80.4.60.88
+80.88.242.32
+80.89.58.59
+80.90.160.194
+80.98.201.218
+81.101.145.245
+81.101.146.0
+81.104.137.210
+81.104.140.27
+81.104.254.45
+81.177.3.10
+81.18.116.70
+81.189.106.138
+81.192.153.91
+81.203.116.165
+81.21.97.68
+81.211.104.58
+81.214.184.188
+81.31.157.38
+81.34.252.228
+81.43.23.51
+81.52.167.82
+81.82.192.93
+81.96.121.31
+81.96.127.75
+81.97.147.154
+81.98.109.201
+82.0.100.211
+82.0.70.181
+82.11.211.202
+82.12.101.34
+82.12.118.67
+82.13.13.173
+82.13.85.245
+82.130.196.153
+82.130.196.97
+82.130.246.64
+82.146.33.201
+82.154.126.143
+82.158.219.108
+82.200.165.143
+82.21.1.166
+82.21.184.178
+82.21.51.247
+82.22.138.43
+82.230.7.246
+82.234.51.250
+82.238.32.72
+82.239.187.75
+82.24.15.141
+82.24.250.31
+82.245.149.3
+82.28.185.247
+82.28.30.130
+82.29.230.10
+82.3.162.235
+82.3.225.58
+82.3.97.212
+82.32.221.58
+82.33.108.2
+82.33.117.189
+82.33.168.194
+82.33.46.103
+82.33.67.71
+82.34.108.122
+82.34.224.141
+82.35.201.216
+82.35.243.181
+82.35.91.170
+82.36.209.11
+82.36.86.70
+82.37.169.145
+82.38.36.40
+82.39.199.238
+82.4.211.107
+82.4.47.67
+82.40.215.66
+82.40.28.187
+82.40.48.179
+82.41.10.6
+82.41.198.251
+82.41.21.126
+82.41.221.10
+82.41.4.227
+82.41.5.12
+82.41.56.62
+82.41.57.26
+82.42.57.203
+82.43.58.68
+82.43.63.99
+82.44.235.63
+82.44.34.27
+82.44.97.222
+82.45.110.245
+82.45.117.238
+82.45.253.25
+82.45.254.205
+82.45.59.203
+82.46.144.165
+82.46.169.181
+82.46.23.204
+82.46.44.15
+82.47.59.57
+82.5.60.63
+82.6.16.219
+82.6.69.14
+82.7.105.26
+82.76.17.46
+82.8.80.191
+82.9.52.183
+83.100.149.29
+83.111.81.109
+83.142.23.194
+83.17.123.186
+83.2.212.9
+83.218.188.208
+83.220.195.232
+83.231.34.133
+83.231.34.192
+83.231.34.236
+83.236.157.231
+83.36.162.217
+83.61.22.207
+83.64.115.103
+83.70.106.206
+83.85.27.225
+83.96.39.196
+84.12.135.98
+84.194.92.1
+84.198.148.132
+84.198.202.74
+84.204.73.154
+84.235.0.182
+84.236.171.66
+84.247.24.127
+84.255.246.20
+84.67.132.233
+84.71.161.123
+85.114.131.54
+85.131.161.84
+85.134.160.128
+85.142.20.122
+85.168.233.221
+85.214.52.253
+85.214.59.79
+85.219.5.109
+85.24.89.199
+85.31.91.114
+85.70.156.138
+85.84.213.189
+86.0.224.116
+86.10.109.253
+86.10.147.26
+86.101.185.109
+86.101.185.112
+86.101.185.97
+86.101.185.99
+86.105.181.238
+86.109.100.80
+86.11.208.239
+86.12.57.51
+86.12.7.19
+86.125.142.141
+86.15.193.138
+86.2.31.207
+86.20.87.54
+86.21.200.186
+86.22.7.232
+86.24.213.144
+86.25.180.145
+86.3.40.90
+86.4.20.251
+86.4.25.128
+86.42.180.157
+86.42.243.36
+86.46.156.172
+86.61.76.7
+86.9.124.75
+87.116.164.85
+87.120.67.39
+87.252.3.67
+87.66.29.96
+87.86.13.29
+87.94.43.58
+88.104.209.63
+88.104.67.106
+88.108.166.245
+88.110.83.81
+88.12.16.211
+88.165.169.130
+88.171.218.44
+88.172.20.212
+88.173.201.9
+88.173.228.213
+88.174.252.233
+88.183.152.141
+88.191.63.104
+88.191.63.27
+88.191.66.131
+88.191.69.101
+88.191.98.15
+88.198.57.182
+88.2.237.249
+88.208.219.155
+88.22.10.27
+88.26.196.190
+88.5.211.84
+89.140.79.175
+89.146.71.82
+89.163.30.175
+89.19.172.22
+89.206.8.242
+89.21.137.70
+89.212.253.19
+89.234.27.15
+89.241.213.95
+89.248.194.212
+89.29.195.27
+89.39.142.121
+89.96.169.141
+90.155.218.74
+90.157.115.140
+90.173.78.226
+90.199.136.7
+91.110.151.89
+91.121.147.12
+91.121.48.207
+91.121.91.61
+91.151.106.127
+91.203.136.191
+91.235.51.238
+91.235.51.247
+91.78.100.114
+92.233.166.55
+92.233.226.34
+92.233.3.150
+92.234.144.16
+92.235.253.182
+92.236.102.208
+92.236.137.151
+92.236.16.51
+92.236.18.113
+92.236.222.129
+92.236.249.98
+92.236.26.72
+92.237.70.251
+92.237.9.240
+92.238.148.101
+92.238.184.16
+92.238.25.211
+92.238.40.83
+92.239.120.214
+92.243.17.151
+92.63.49.201
+92.64.178.98
+92.9.76.236
+93.156.180.97
+93.184.0.20
+93.92.34.238
+94.102.60.89
+94.23.192.228
+94.23.81.70
+94.25.81.45
+95.62.147.8
+96.21.139.56
+96.28.116.40
+96.28.160.240
+96.3.152.82
+96.3.172.29
+97.85.152.126
+97.87.65.118
+97.91.188.113
+97.97.255.95
+98.126.15.16
+98.126.15.27
+98.126.27.165
+98.141.23.139
+98.155.147.62
+98.16.253.47
+98.163.204.145
+98.164.75.175
+98.165.245.250
+98.166.26.87
+98.169.171.231
+98.181.60.131
+98.181.63.133
+98.192.95.181
+98.197.219.216
+98.202.107.151
+98.202.188.75
+98.204.164.207
+98.206.20.88
+98.208.46.176
+98.210.139.101
+98.210.147.111
+98.223.204.15
+98.229.212.211
+98.230.6.223
+98.233.228.159
+98.240.186.255
+98.243.17.13
+98.244.161.239
+98.28.33.20
+99.155.153.203
+99.199.237.158
+99.225.136.21
+99.228.104.199
+99.232.137.243
+99.232.189.8
+99.237.129.44
+99.242.140.117
+99.247.210.73
+99.254.157.217
+99.254.203.191
+103.94.125.244
+177.126.218.67
+176.122.98.51
+196.27.116.162
+93.188.45.157
+103.37.30.66
+75.149.141.145
+160.202.157.254
+84.10.1.82
+181.29.65.2
+75.71.253.42
+117.74.124.129
+79.106.41.15
+5.175.69.113
+93.123.196.160
+159.255.165.221
+200.98.141.76
+108.61.191.181
+195.178.207.241
+176.111.33.152
+202.142.169.123
+95.78.113.84
+194.187.216.182
+178.57.82.102
+45.6.100.90
+68.15.42.194
+186.221.152.223
+46.99.255.235
+180.183.221.144
+91.214.179.24
+110.37.201.75
+128.199.239.109
+200.29.101.77
+31.211.130.169
+72.47.105.234
+185.29.255.195
+185.5.183.101
+175.111.182.153
+103.225.174.13
+140.227.70.107
+188.191.29.15
+95.137.240.222
+85.187.194.13
+91.186.121.19
+181.114.56.242
+105.22.72.26
+81.82.200.134
+45.71.240.82
+222.124.2.186
+91.92.79.137
+190.147.208.143
+180.250.165.200
+79.173.97.45
+212.90.180.154
+85.172.109.18
+69.85.70.37
+94.102.124.139
+217.126.85.147
+62.213.57.218
+12.52.30.83
+41.164.31.154
+36.72.34.50
+202.6.224.52
+139.255.95.194
+46.19.47.114
+24.134.35.197
+190.85.70.110
+202.91.82.81
+213.174.10.58
+185.247.136.198
+201.190.190.250
+46.40.7.131
+89.165.218.82
+170.239.46.145
+190.185.180.166
+103.83.205.57
+95.93.98.73
+36.89.143.185
+202.52.126.3
+212.46.220.214
+201.247.175.50
+191.102.64.15
+27.123.221.51
+188.17.149.0
+176.99.110.182
+103.197.49.14
+103.106.119.154
+89.31.45.115
+93.187.161.119
+103.254.209.68
+195.78.101.162
+187.19.62.7
+197.232.51.81
+162.253.153.80
+82.144.130.13
+14.1.102.218
+36.89.53.195
+170.254.229.154
+139.0.29.18
+78.107.250.181
+89.135.125.133
+177.85.91.40
+37.187.116.199
+96.74.196.249
+85.175.226.106
+103.218.26.110
+197.253.67.68
+49.236.220.238
+145.239.86.210
+170.80.86.1
+95.46.1.130
+90.150.181.35
+77.247.88.10
+193.93.49.193
+181.143.51.50
+196.192.185.142
+109.74.50.14
+106.12.32.43
+91.197.204.139
+189.205.61.147
+103.241.205.66
+212.164.234.207
+83.223.132.172
+31.28.0.204
+81.16.246.44
+69.206.51.199
+76.10.246.164
+185.15.0.187
+85.105.222.29
+77.242.18.96
+181.39.165.155
+197.210.152.38
+31.45.246.187
+119.40.87.94
+202.179.186.238
+37.252.65.183
+93.170.113.241
+103.232.67.18
+154.117.159.226
+66.210.170.9
+138.41.25.163
+69.51.6.201
+36.89.235.35
+78.130.241.7
+109.230.60.3
+176.192.124.98
+201.150.149.79
+87.117.1.150
+2.92.106.53
+178.134.79.18
+185.162.62.206
+125.236.241.235
+118.144.114.134
+118.91.181.153
+193.85.30.78
+170.79.9.54
+190.104.215.14
+31.22.29.229
+209.206.113.193
+37.53.83.40
+36.67.8.245
+200.13.243.178
+82.114.68.58
+154.117.157.5
+193.218.149.91
+85.66.27.165
+185.75.206.131
+154.119.49.238
+84.42.56.237
+95.165.182.18
+181.129.148.138
+193.43.95.139
+83.19.160.122
+185.98.233.39
+91.202.207.110
+191.7.212.178
+80.211.109.96
+103.194.250.182
+188.242.249.116
+181.143.36.163
+92.51.75.203
+45.234.16.138
+183.88.174.196
+206.189.217.206
+80.90.25.160
+185.62.188.84
+95.64.253.177
+91.102.80.82
+139.255.113.98
+92.86.32.150
+87.229.89.144
+196.44.98.162
+159.192.97.83
+45.221.72.58
+85.100.108.84
+81.161.67.240
+83.208.168.199
+213.6.139.42
+24.139.73.82
+131.72.96.202
+85.200.245.65
+95.79.116.84
+181.129.45.154
+110.136.119.140
+195.222.61.29
+91.93.168.227
+178.140.116.185
+83.146.67.32
+159.203.174.2
+182.16.173.74
+5.188.102.81
+27.123.1.46
+209.203.6.246
+176.215.197.128
+109.188.64.248
+27.116.60.250
+74.93.145.1
+185.190.149.34
+212.96.201.128
+24.113.168.225
+103.87.236.154
+92.247.187.132
+24.226.159.195
+45.64.158.150
+187.180.18.52
+141.105.35.11
+185.141.11.118
+145.239.90.169
+186.225.50.211
+114.57.238.254
+177.10.21.154
+103.206.230.26
+83.169.214.53
+46.238.248.116
+193.178.190.173
+182.52.134.121
+177.39.56.223
+217.61.172.12
+95.167.241.242
+85.133.207.14
+103.111.83.26
+193.107.228.222
+80.106.247.145
+91.189.131.114
+184.69.57.210
+194.125.224.43
+200.75.204.150
+43.255.114.53
+111.119.225.168
+183.91.66.210
+103.65.192.211
+181.48.203.198
+195.112.122.197
+185.91.166.83
+190.42.32.154
+124.191.118.110
+37.57.163.30
+173.219.56.28
+94.45.155.45
+5.39.79.90
+181.196.50.130
+35.184.125.195
+104.248.122.203
+181.28.17.171
+178.150.191.73
+187.28.39.146
+109.236.211.171
+86.120.46.88
+103.112.9.31
+85.106.7.4
+41.50.88.56
+5.56.122.183
+103.100.80.42
+43.245.186.105
+140.190.17.195
+45.114.68.123
+5.53.114.152
+42.115.72.254
+203.82.197.34
+182.52.51.13
+103.255.234.189
+37.150.188.120
+93.170.119.242
+93.86.163.238
+45.126.46.140
+36.66.61.155
+188.32.95.148
+202.148.2.254
+77.232.137.35
+90.154.120.202
+85.207.99.213
+36.89.132.210
+156.236.71.29
+195.138.91.102
+84.241.41.150
+112.78.143.26
+103.254.59.122
+93.87.41.14
+170.178.151.250
+197.149.128.56
+167.114.250.199
+223.197.56.102
+52.68.71.75
+103.103.182.19
+196.203.55.18
+41.60.216.43
+180.94.87.157
+103.5.172.182
+103.251.176.106
+77.242.26.57
+130.0.31.226
+91.187.85.98
+105.174.18.118
+105.174.19.194
+181.170.223.14
+200.91.48.20
+5.77.240.130
+37.252.68.84
+109.75.34.188
+35.194.213.161
+104.199.234.56
+115.70.2.73
+101.167.169.229
+202.174.46.113
+178.189.11.134
+77.119.245.150
+91.133.123.12
+109.127.9.96
+85.132.18.222
+91.135.241.110
+64.150.235.214
+103.216.59.81
+49.0.39.153
+115.127.63.10
+63.175.159.29
+37.17.12.131
+86.57.181.122
+78.20.210.244
+81.82.209.76
+94.226.211.106
+160.238.136.125
+138.185.76.78
+160.238.136.121
+41.74.9.238
+41.86.244.89
+201.131.41.254
+181.114.115.105
+81.93.93.251
+81.93.73.27
+185.12.79.59
+83.143.31.254
+83.143.26.70
+129.205.201.56
+201.74.174.141
+177.136.5.166
+187.108.114.108
+95.158.153.145
+89.106.101.191
+79.110.125.201
+41.216.148.140
+154.73.40.70
+196.2.15.144
+196.2.10.56
+111.118.150.193
+119.82.253.32
+103.239.54.188
+41.204.84.161
+169.239.40.1
+24.122.184.158
+197.149.128.233
+169.239.123.69
+200.54.44.140
+179.57.108.159
+200.27.66.222
+101.76.214.72
+221.218.102.146
+190.85.153.139
+201.236.248.216
+200.58.213.178
+197.255.179.179
+41.75.76.75
+41.190.232.158
+41.243.13.50
+170.81.34.22
+186.26.121.98
+186.4.4.90
+195.29.45.100
+213.147.102.122
+194.30.157.117
+31.209.104.71
+193.86.125.62
+84.42.202.253
+89.111.104.109
+212.112.129.203
+158.248.219.169
+93.176.85.228
+196.201.206.219
+154.66.245.47
+199.127.197.12
+190.166.249.44
+190.211.182.7
+190.122.97.138
+180.189.167.34
+103.55.48.170
+181.211.97.18
+201.183.249.226
+186.178.10.20
+197.51.146.78
+41.65.99.133
+200.89.87.242
+168.227.20.27
+190.53.46.134
+105.235.235.156
+95.153.30.58
+80.79.114.240
+95.210.45.189
+130.117.175.134
+130.117.169.119
+95.216.160.51
+95.216.95.226
+163.172.146.169
+41.78.243.198
+41.78.243.218
+31.192.17.25
+188.121.193.44
+185.164.111.197
+46.4.24.166
+94.16.120.75
+102.177.101.9
+41.139.51.98
+62.103.22.100
+185.186.84.66
+201.216.168.238
+190.115.9.113
+160.119.130.10
+41.242.90.178
+160.119.129.42
+186.1.206.99
+190.185.119.91
+190.6.205.134
+122.115.78.240
+37.191.6.61
+109.74.61.67
+46.107.226.220
+27.0.183.67
+103.194.250.110
+219.91.142.237
+36.89.75.57
+36.37.124.226
+185.129.212.4
+212.86.75.9
+37.238.132.186
+37.237.63.82
+89.101.215.90
+52.169.139.131
+109.226.17.8
+109.226.26.134
+192.116.49.15
+212.66.97.185
+185.26.65.82
+208.131.186.162
+74.116.59.8
+208.163.39.218
+61.113.193.98
+92.46.54.114
+91.185.2.102
+41.139.141.254
+217.21.125.225
+41.90.102.34
+222.121.116.26
+27.255.91.146
+94.128.135.77
+158.181.171.9
+92.245.114.23
+158.181.19.120
+188.112.142.182
+195.13.161.141
+185.142.43.35
+185.104.252.10
+185.9.137.114
+154.66.109.182
+41.191.204.146
+41.191.205.29
+197.215.217.122
+196.250.176.67
+197.215.217.150
+62.68.34.86
+91.187.188.115
+78.157.79.120
+104.244.72.171
+195.218.3.241
+95.86.56.113
+92.55.84.202
+62.162.106.49
+41.77.23.238
+196.216.13.27
+41.190.95.66
+154.66.122.142
+211.24.98.29
+182.54.207.74
+217.64.109.231
+197.155.158.22
+196.200.60.142
+212.56.152.162
+88.203.36.209
+41.72.213.22
+41.72.192.170
+197.231.186.148
+148.243.240.156
+177.234.0.218
+93.116.185.57
+95.65.1.200
+89.28.101.79
+202.21.124.226
+202.131.248.66
+196.92.3.193
+165.90.67.102
+196.3.97.86
+196.3.97.68
+203.81.74.46
+122.248.100.13
+196.20.12.29
+196.20.12.9
+196.20.12.41
+103.1.94.213
+103.235.199.33
+202.166.196.34
+185.179.204.210
+185.179.204.226
+185.179.204.175
+190.4.186.20
+190.2.132.17
+203.147.79.184
+202.27.212.17
+114.134.164.166
+202.49.183.168
+165.98.135.6
+186.1.6.62
+197.234.45.230
+105.112.83.35
+155.93.122.186
+195.204.130.149
+81.166.242.208
+46.183.169.64
+125.209.118.42
+202.142.191.75
+213.6.69.113
+213.6.136.118
+213.6.198.230
+190.218.77.236
+179.63.195.140
+103.103.182.22
+203.83.20.53
+181.40.40.166
+190.128.228.54
+190.52.177.128
+200.37.16.253
+45.5.56.62
+202.57.63.206
+210.4.97.193
+111.125.87.199
+195.205.218.53
+46.238.120.149
+93.108.234.238
+217.129.163.102
+193.126.23.235
+24.139.244.238
+192.254.109.11
+65.38.222.162
+95.76.196.92
+82.79.83.78
+95.154.72.55
+95.31.13.55
+41.242.140.157
+196.223.246.11
+212.69.18.150
+178.222.246.55
+154.70.175.155
+41.86.54.160
+41.86.57.65
+196.216.220.204
+196.216.220.130
+150.107.124.32
+150.107.124.37
+150.107.124.35
+213.81.178.97
+86.110.229.38
+5.22.154.13
+188.230.234.67
+77.38.21.145
+77.94.144.162
+103.21.231.132
+103.21.231.131
+103.21.230.67
+41.79.198.36
+154.73.45.206
+41.162.53.130
+196.22.229.210
+41.193.101.122
+160.119.211.50
+69.63.67.44
+160.119.210.180
+84.217.82.227
+78.73.14.128
+82.145.149.5
+217.11.47.9
+179.43.144.19
+177.154.139.196 \ No newline at end of file
diff --git a/cgi/quotes.conf b/cgi/quotes.conf
new file mode 100644
index 0000000..faf5221
--- /dev/null
+++ b/cgi/quotes.conf
@@ -0,0 +1,13 @@
+Eres una buena persona.
+Fue un mensaje de calidad.
+ï½·ï¾€â”â”â”â”â”\( ゚∀゚ )ï¼â”â”â”â”â”!!!
+Te invitaría a un café.
+Plataformas del futuro para la web 1.0.
+Plataformas del pasado para la web 2.0.
+Suenas como un bot muy desarrollado.
+Elegiste bien. Elegiste calidad.
+Gracias por usar Internet.
+Gracias por tu papiro.
+`·.¸¸.·´´¯`··._.·[GrAcIaS pOr El PoSt]`·.¸¸.·´´¯`··._.·
+Ha sido un éxito.
+Funcionó. \ No newline at end of file
diff --git a/cgi/template.py b/cgi/template.py
new file mode 100644
index 0000000..0a7c530
--- /dev/null
+++ b/cgi/template.py
@@ -0,0 +1,117 @@
+# coding=utf-8
+import tenjin
+import random
+import re
+from tenjin.helpers import * # Used when templating
+
+from settings import Settings
+from database import *
+
+def renderTemplate(template, template_values={}, mobile=False, noindex=False):
+ """
+ Run Tenjin on the supplied template name, with the extra values
+ template_values (if supplied)
+ """
+ values = {
+ "title": Settings.NAME,
+ "board": None,
+ "board_name": None,
+ "board_long": None,
+ "is_page": "false",
+ "noindex": None,
+ "replythread": 0,
+ "home_url": Settings.HOME_URL,
+ "boards_url": Settings.BOARDS_URL,
+ "images_url": Settings.IMAGES_URL,
+ "static_url": Settings.STATIC_URL,
+ "cgi_url": Settings.CGI_URL,
+ "banner_url": None,
+ "banner_width": None,
+ "banner_height": None,
+ "disable_name": None,
+ "disable_subject": None,
+ "styles": Settings.STYLES,
+ "styles_default": Settings.STYLES_DEFAULT,
+ "txt_styles": Settings.TXT_STYLES,
+ "txt_styles_default": Settings.TXT_STYLES_DEFAULT,
+ "pagenav": "",
+ "reports_enable": Settings.REPORTS_ENABLE,
+ "force_css": ""
+ }
+
+ engine = tenjin.Engine(pp=[tenjin.TrimPreprocessor(True)])
+ board = Settings._.BOARD
+
+ #if board:
+ if template in ["board.html", "threadlist.html", "catalog.html", "kako.html", "paint.html"] or template[0:3] == "txt":
+ # TODO HACK
+ if board['dir'] == 'world' and not mobile and (template == 'txt_board.html' or template == 'txt_thread.html'):
+ template = template[:-4] + 'en.html'
+ elif board['dir'] == '2d' and template == 'board.html' and not mobile:
+ template = template[:-4] + 'jp.html'
+ elif board['dir'] == '0' and template == 'board.html' and not mobile:
+ template = template[:-4] + '0.html'
+
+ try:
+ banners = Settings.banners[board['dir']]
+ if banners:
+ banner_width = Settings.banners[board['dir']]
+ banner_height = Settings.banners[board['dir']]
+ except KeyError:
+ banners = Settings.banners['default']
+ banner_width = Settings.banners['default']
+ banner_height = Settings.banners['default']
+
+ values.update({
+ "board": board["dir"],
+ "board_name": board["name"],
+ "board_long": board["longname"],
+ "board_type": board["board_type"],
+ "oek_finish": 0,
+ "disable_name": (board["disable_name"] == '1'),
+ "disable_subject": (board["disable_subject"] == '1'),
+ "default_subject": board["subject"],
+ "postarea_desc": board["postarea_desc"],
+ "postarea_extra": board["postarea_extra"],
+ "allow_images": (board["allow_images"] == '1'),
+ "allow_image_replies": (board["allow_image_replies"] == '1'),
+ "allow_noimage": (board["allow_noimage"] == '1'),
+ "allow_spoilers": (board["allow_spoilers"] == '1'),
+ "allow_oekaki": (board["allow_oekaki"] == '1'),
+ "archive": (board["archive"] == '1'),
+ "force_css": board["force_css"],
+ "noindex": (board["secret"] == '1'),
+ "useid": board["useid"],
+ "maxsize": board["maxsize"],
+ "maxage": board["maxage"],
+ "maxdimensions": board["thumb_px"],
+ "supported_filetypes": board["filetypes_ext"],
+ "prevrange": '',
+ "nextrange": '',
+ })
+ else:
+ banners = Settings.banners['default']
+ banner_width = Settings.banners['default']
+ banner_height = Settings.banners['default']
+
+ if Settings.ENABLE_BANNERS:
+ if len(banners) > 1:
+ random_number = random.randrange(0, len(banners))
+ BANNER_URL = Settings.banners_folder + banners[random_number][0]
+ BANNER_WIDTH = banners[random_number][1]
+ BANNER_HEIGHT = banners[random_number][2]
+ else:
+ BANNER_URL = Settings.banners_folder + banners[0][0]
+ BANNER_WIDTH = banners[0][1]
+ BANNER_HEIGHT = banners[0][2]
+
+ values.update({"banner_url": BANNER_URL, "banner_width": BANNER_WIDTH, "banner_height": BANNER_HEIGHT})
+
+ values.update(template_values)
+
+ if mobile:
+ template_folder = "templates/mobile/"
+ else:
+ template_folder = "templates/"
+
+ return engine.render(template_folder + template, values) \ No newline at end of file
diff --git a/cgi/templates/anarkia.html b/cgi/templates/anarkia.html
new file mode 100644
index 0000000..3ded9da
--- /dev/null
+++ b/cgi/templates/anarkia.html
@@ -0,0 +1,329 @@
+<?py include('templates/base_top.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<style>.anarkiahead {width:1000px; text-align:left}
+.anarkiahead h2 {margin-top: 0}
+.anarkiamenu a {font-size:20pt;display:inline-block;width:300px;padding:10px 0}
+.logs {font-size:small;max-height:300px;overflow-y:auto;width:600px}
+.long {white-space:nowrap}
+.full {width:100%}
+.return {font-size:24pt}</style>
+<center>
+<div class="replymode" style="font-size:26pt;color:red;font-weight:bold">ⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶ</div>
+<br />
+<?py if mode == 0: ?>
+ <div class="anarkiahead">
+ <h2 style="border-bottom:1px solid;width:100%;">Anarkía @ B.a.I.</h2>
+ <p>Anarkía es una sección especial sin moderación y con acceso libre a su panel de administración.</p>
+ <ul>
+ <li>El staff de B.a.I. no interferirá de ninguna manera en esta sección y cualquiera es libre de modificar
+ sus parámetros, de eliminar mensajes o banear usuarios dentro de ella.</li>
+ <li>Los hilos de otras secciones que sean eliminados por su baja calidad, denuncias u otra razón, caerán por defecto a esta sección.</li>
+ <li>Los bans en esta sección son independientes del resto del sitio. Es decir, usuarios baneados en BaI son libres de usar esta sección.</li>
+ <li>Cualquier problema en su funcionamiento por favor reportar en la sección <a href="/bai/">Meta</a>.</li>
+ </div>
+ <div class="anarkiamenu">
+ <a href="#{cgi_url}anarkia/opt"><img src="#{boards_url}anarkia/opt.jpg" width="250" height="175"><br />Opciones generales</a>
+ <a href="#{cgi_url}anarkia/mod"><img src="#{boards_url}anarkia/mod.jpg" width="250" height="175"><br />Panel de moderación</a>
+ <a href="#{cgi_url}anarkia/css"><img src="#{boards_url}anarkia/css.jpg" width="250" height="175"><br />Editar CSS</a>
+ <br />
+ <a href="#{cgi_url}anarkia/emojis"><img src="#{boards_url}anarkia/emojis.jpg" width="250" height="175"><br />Emojis</a>
+ <a href="#{cgi_url}anarkia/bans"><img src="#{boards_url}anarkia/bans.jpg" width="250" height="175"><br />Bans</a>
+ <a href="#{cgi_url}anarkia/type"><img src="#{boards_url}anarkia/type.jpg" width="250" height="175"><br />Cambiar tipo de board</a>
+ </div>
+ <hr />
+ <input type="hidden" name="board" value="anarkia" />
+ <div class="logs">
+ <table class="managertable full">
+ <tr><th colspan="2">Logs</th></tr>
+ <tr><th>Fecha</th><th class="full">Acción</th></tr>
+ <?py for log in logs: ?>
+ <tr><td class="date" data-unix="${log['timestamp']}">${log['timestamp_formatted']}</td><td>${log['action']}</td></tr>
+ <?py #endfor ?>
+ </table>
+ </div>
+ <hr /><a href="#{boards_url}anarkia" class="return">Volver a la sección</a>
+<?py elif mode == 1: ?>
+<div class="replymode">Opciones de Board</div>
+<form action="#{cgi_url}anarkia/opt" method="post">
+<table>
+ <tr>
+ <td class="postblock">Nombre de sección</td>
+ <td><input type="text" name="longname" size="50" value="${boardopts['longname']}" maxlength="128" class="full" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Descripción</td>
+ <td>
+ <textarea id="patop" name="postarea_desc" rows="10" cols="50" class="full" oninput="pvw('patop')">${boardopts['postarea_desc']}</textarea>
+ <div id="p_patop" style="border:1px dotted gray;width:100%;"></div>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Caja extra</td>
+ <td><textarea name="postarea_extra" rows="5" cols="50" class="full">${boardopts['postarea_extra']}</textarea></td>
+ </tr>
+ <tr>
+ <td class="postblock">Nombre por defecto</td>
+ <td><input type="text" name="anonymous" size="50" maxlength="128" value="${boardopts['anonymous']}" class="full" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Título por defecto</td>
+ <td><input type="text" name="subject" size="50" maxlength="64" value="${boardopts['subject']}" class="full" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Mensaje por defecto</td>
+ <td><input type="text" name="message" size="50" maxlength="128" value="${boardopts['message']}" class="full" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">ID</td>
+ <td>
+ <select name="useid" class="full">
+ <option value="0">Desactivado</option>
+ <option value="1"#{selected(boardopts['useid'] == '1')}>Activado</option>
+ <option value="2"#{selected(boardopts['useid'] == '2')}>Activado siempre</option>
+ <option value="3"#{selected(boardopts['useid'] == '3')}>Activado siempre, detallado</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Desactivar nombre</td>
+ <td><input type="checkbox" name="disable_name" id="noname" value="1"#{checked(boardopts['disable_name'] == '1')} /><label for="noname"></label></td>
+ </tr>
+ <tr>
+ <td class="postblock">Desactivar asunto</td>
+ <td><input type="checkbox" name="disable_subject" id="nosub" value="1"#{checked(boardopts['disable_subject'] == '1')} /><label for="nosub"></label></td>
+ </tr>
+ <tr>
+ <td class="postblock">Permitir crear hilos sin imagen</td>
+ <td><input type="checkbox" name="allow_noimage" id="noimgallow" value="1"#{checked(boardopts['allow_noimage'] == '1')} /><label for="noimgallow"></label></td>
+ </tr>
+ <tr>
+ <td class="postblock">Permitir subida</td>
+ <td><input type="checkbox" name="allow_images" id="img" value="1"#{checked(boardopts['allow_images'] == '1')} /><label for="img">Al crear un hilo</label><br /><input type="checkbox" name="allow_image_replies" id="imgres" value="1"#{checked(boardopts['allow_image_replies'] == '1')} /><label for="imgres">Al responder</label></td>
+ </tr>
+ <tr>
+ <td class="postblock">Tipos de archivo</td>
+ <td>
+ <?py for filetype in filetypes: ?>
+ <input type="checkbox" name="filetype#{filetype['ext']}" id="#{filetype['ext']}" value="1"#{checked(filetype['ext'] in supported_filetypes)} /><label for="#{filetype['ext']}">${filetype['ext'].upper()}</label><br />
+ <?py #endfor ?>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Tamaño máximo <span style="font-weight:normal;">(KB)</span></td>
+ <td><input type="text" name="maxsize" value="#{boardopts['maxsize']}" maxlength="5" size="11" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Dimensión de miniatura <span style="font-weight:normal;">(px)</span></td>
+ <td><input type="text" name="thumb_px" value="#{boardopts['thumb_px']}" maxlength="3" size="11" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Hilos en página frontal</td>
+ <td><input type="text" name="numthreads" value="#{boardopts['numthreads']}" maxlength="2" size="11" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Respuestas a mostrar</td>
+ <td><input type="text" name="numcont" value="#{boardopts['numcont']}" maxlength="2" size="11" /></td>
+ </tr>
+</table>
+<hr />
+<input type="submit" value="Guardar cambios" />
+</form>
+<hr />
+<a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 2: ?>
+<div class="replymode">Denuncias</div>
+<?py if reports: ?>
+ <table class="managertable" style="max-width:1000px">
+ <tr>
+ <th>Post</th>
+ <th>Fecha</th>
+ <th style="min-width:200px;">Razón</th>
+ </tr>
+ <?py for r in reports: ?>
+ <tr>
+ <td><a href="?thread=#{r['parentid'] if r['parentid'] != "0" else r['postid']}##{r['postid']}" style="font-weight:bold">#{r['postid']}</td>
+ <td>${r['timestamp_formatted']}</td>
+ <td>#{r['reason']}</a></td>
+ </tr>
+ <?py #endfor ?>
+ </table>
+<?py else: ?>
+ No hay denuncias.<br />
+<?py #endif ?>
+<br />
+<div class="replymode">Lista de hilos</div>
+<table class="managertable" style="max-width:1000px;">
+<tr>
+ <th>#</th>
+ <th>ID</th>
+ <th style="width:50%;">Asunto</th>
+ <th>Fecha</th>
+ <th style="width:50%;">Mensaje</th>
+ <th>Posts</th>
+ <th>Acción</th>
+</tr>
+<?py i = 1 ?>
+<?py for thread in threads: ?>
+<tr>
+ <td>#{i}</td>
+ <td>#{thread['id']}</td>
+ <td><a href="?thread=#{thread['id']}" style="font-size:16pt;"><b>#{thread['subject'][:30]}</b></a></td>
+ <td>#{thread['timestamp_formatted'][:21]}</td>
+ <td>${thread['message'][:250]}</td>
+ <td>#{thread['length']}</td>
+ <td>[<a href="?lock=#{thread['id']}">#{"<b>Abrir</b>" if thread['locked'] == "1" else "Cerrar"}</a>]</td>
+</tr>
+<?py i += 1 ?>
+<?py #endfor ?>
+</table>
+<hr /><a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 3: ?>
+<div class="replymode" style="font-size:16pt">Hilo: ${posts[0]['subject']} (#{posts[0]['length']})</div>
+<table class="managertable" style="width:1000px;">
+ <tr>
+ <th>#</th>
+ <th>ID</th>
+ <th>Fecha</th>
+ <th>Nombre</th>
+ <th>Mensaje</th>
+ <th>Usuario</th>
+ </tr>
+<?py i = 1 ?>
+<?py for p in posts: ?>
+<?py if p['IS_DELETED'] == '0': ?>
+ <tr>
+ <td>#{i}</td>
+ <td class="long">
+ <b>#{p['id']}</b>
+ <?py if p['parentid'] != '0': ?>
+ [<a href="?del=#{p['id']}">Eliminar</a>]
+ <?py else: ?>
+ [<a href="?lock=#{p['id']}">#{"<b>Abrir</b>" if p['locked'] == "1" else "Cerrar"}</a>]
+ <?py #endif ?>
+ </td>
+ <td>${p['timestamp_formatted']}</td>
+ <td><span class="postername">${p['name']}</span></td>
+ <td>${p['message']}</td>
+ <td class="long">#{p['ip'][:4]} [<a href="?ban=#{p['id']}">Ban</a>]</td>
+ </tr>
+<?py else: ?>
+ <tr>
+ <td>#{i}</td>
+ <td class="long"><b>#{p['id']}</b> [<a href="?restore=#{p['id']}">Recuperar</a>]</td>
+ <td colspan="4">Eliminado.</td>
+ </tr>
+<?py #endif ?>
+<?py i += 1 ?>
+<?py #endfor ?>
+</table>
+<hr /><a href="#{cgi_url}anarkia/mod" class="return">Volver al panel de moderación</a>
+<?py elif mode == 4: ?>
+<div class="replymode">Colocar ban</div>
+<form action="#{cgi_url}anarkia/mod" name="banform" method="post">
+<input type="hidden" name="banto" value="#{post['id']}" />
+<table>
+ <tr><td class="postblock">Ban para usuario</td><td><b>#{post['ip'][-4:]}</b></td></tr>
+ <tr><td class="postblock">Mensaje</td><td><textarea name="reason" class="full" maxlength="512"></textarea></td></tr>
+ <tr><td class="postblock">Ciego</td><td><input type="checkbox" name="blind" value="1" checked="checked" /></td></tr>
+ <tr><td class="postblock">Expira en <span style="font-weight:normal;">(segundos)</span></td>
+ <td><input type="text" name="seconds" class="full" value="3600" maxlength="8" /><br />
+ <a href="#" onclick="document.banform.seconds.value='0';return false;">Nunca</a>
+ <a href="#" onclick="document.banform.seconds.value='3600';return false;">1hr</a>
+ <a href="#" onclick="document.banform.seconds.value='43200';return false;">12hr</a>
+ <a href="#" onclick="document.banform.seconds.value='86400';return false;">1d</a>
+ <a href="#" onclick="document.banform.seconds.value='259200';return false;">3d</a>
+ <a href="#" onclick="document.banform.seconds.value='604800';return false;">1w</a>
+ <a href="#" onclick="document.banform.seconds.value='2592000';return false;">1m</a>
+ <a href="#" onclick="document.banform.seconds.value='31536000';return false;">1yr</a>
+ </td>
+ </tr>
+ <tr><td colspan="2"><input type="submit" value="Banear" class="full" /></td></tr>
+</table>
+</form>
+<hr />
+<a href="#{cgi_url}anarkia/mod" class="return">Volver al panel de moderación</a>
+<?py elif mode == 5: ?>
+<div class="replymode">Lista de bans</div>
+<table class="managertable" style="max-width:1000px;">
+<tr>
+ <th>ID</th>
+ <th>Usuario</th>
+ <th>Puesto</th>
+ <th>Expira</th>
+ <th>Ciego</th>
+ <th style="min-width:200px;">Razón</th>
+ <th>Acción</th>
+</tr>
+<?py if bans: ?>
+ <?py for ban in bans: ?>
+ <tr>
+ <td class="long">#{ban['id']}</td>
+ <td>#{ban['ip'][-4:]}</td>
+ <td>${ban['added']}</td>
+ <td>${ban['until']}</td>
+ <td>${"Sí" if ban['blind'] == "1" else "No"}</td>
+ <td>${ban['reason']}</td>
+ <td>[<a href="?unban=#{ban['id']}">Eliminar ban</a>]</td>
+ </tr>
+ <?py #endfor ?>
+<?py else: ?>
+ <tr><td colspan="7" style="text-align:center;">No hay bans.</td></tr>
+<?py #endif ?>
+</table>
+<hr />
+<a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 6: ?>
+<div class="replymode">Editar CSS</div>
+<p><b>Editando:</b> <code>${basename}</code></p>
+<p style="font-size:small">Dominios permitidos: https://bienvenidoainternet.org https://i.imgur.com</p>
+<form action="#{cgi_url}anarkia/css" name="cssform" method="post" style="display:inline-block;">
+<textarea name="cssfile" cols="100" rows="30">${cssfile}</textarea><br />
+<input type="submit" value="Guardar cambios" class="full" />
+</form>
+<hr />
+<a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 7: ?>
+<div class="replymode">Cambiar tipo de sección</div>
+<h1 style="color:red;font-size:26pt;text-decoration:underline;">ATENCIÓN</h1>
+<p style="font-size:19pt">Estás a punto de cambiar la estructura de esta sección a #{type_do}.</p>
+<p style="font-size:15pt">Esta sección es actualmente un #{type_now} y si prosigues transformarás su estructura a un #{type_do}.</p>
+<p style="color:red;font-size:15pt;">Nótese que este cambio se puede hacer sólo una vez cada 10 minutos.</p>
+<div style="display:inline-block;">
+ <p style="margin-top:0;">¿Seguro que deseas convertir esta sección a #{type_do}?
+ <form method="get">
+ <input type="hidden" name="transform" value="do">
+ <input type="submit" value="Transformar a #{type_do}" class="full" />
+ </form>
+ </p>
+</div>
+<hr />
+<a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 8: ?>
+<div class="replymode">Emojis</div>
+<table class="managertable">
+ <tr><th>Nombre</th><th>Img</th></tr>
+ <?py for emoji in emojis: ?>
+ <tr><td>${emoji['from']}</td><td>#{emoji['to']}</td></tr>
+ <?py #endfor ?>
+</table>
+<hr />
+<form method="post" action="" enctype="multipart/form-data">
+<table>
+<tr>
+ <td class="postblock">Nombre</td>
+ <td><input type="text" name="name" size="15" maxlength="15" class="full" /></td>
+ <td><input type="submit" name="new" value="Agregar emoji" class="full" /></td>
+</tr>
+<tr><td class="postblock">Archivo</td><td colspan="2"><input type="file" name="file" size="15" class="full" /></td></tr>
+</table>
+<small>(Sólo letras y/o números. Máximo: 500x500px, 500 KB.)</small>
+</form>
+<hr />
+<a href="#{cgi_url}anarkia" class="return">Volver al menú</a>
+<?py elif mode == 99: ?>
+<div>${msg}<br /><br /><a href="#{cgi_url}anarkia" class="return">Volver al menú</a></div>
+<?py #endif ?>
+</center>
+<hr />
+<div class="replymode" style="font-size:26pt;color:red;font-weight:bold">ⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶⒶ</div>
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/banned.html b/cgi/templates/banned.html
new file mode 100644
index 0000000..23b6636
--- /dev/null
+++ b/cgi/templates/banned.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Acceso prohibido@B.a.I.</title>
+<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
+<style type="text/css">
+html { text-align:center; }
+body { background:#fe7777;color:#6a0000;display:inline-block;font-size:13pt;max-width:1000px;text-align:left; }
+h1 { color:red;margin:0; }
+h2 { margin:0.5em 0; }
+</style>
+</head>
+<body>
+<h1>Mensaje de Bienvenido a Internet BBS/IB</h1>
+<h2>Se te ha prohibido el acceso :-(</h2>
+<p>¡Tu IP (o rango) ha sido bloqueado!</p>
+<?py if reason != "": ?>
+ <p>La razón dejada fue: <b>#{reason}</b> y tu ban fue puesto el <b>#{added}</b> para las siguientes secciones: <b>#{boards_str}</b></p>
+<?py else: ?>
+ <p>No sabemos qué es lo que pudo causar tu ban, ¿qué hiciste?</p>
+ <p>Tu ban fue puesto el <b>#{added}</b> para las siguientes secciones: <b>#{boards_str}</b></p>
+<?py #endif ?>
+<?py if expire != "": ?>
+ <p>Pero no te preocupes, se te concederá nuevamente el acceso en la siguiente fecha y hora: <b>#{expire}</b>.</p>
+<?py #endif ?>
+<p>Si tu expulsión fue puesta incorrectamente no dudes en <a href="mailto:burocracia@bienvenidoainternet.org">contactarnos</a> dando tu IP, razón y explicación de los hechos.</p>
+<p>¡Gracias por usar Bienvenido a Internet BBS/IB!</p>
+<hr />
+<p><small><i>En muchos casos a pesar de que hayas sido expulsado del sitio se concede el acceso a las secciones <a href="/bai/">Meta</a> y Anarkía. Bajo cualquier consulta o reclamo <a href="mailto:burocracia@bienvenidoainternet.org">contáctanos</a>.</i></small></p>
+<hr />
+<div style="text-align:right;">Bienvenido a Internet 2010-2018</div>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/base_bottom.html b/cgi/templates/base_bottom.html
new file mode 100644
index 0000000..102f8f2
--- /dev/null
+++ b/cgi/templates/base_bottom.html
@@ -0,0 +1,3 @@
+<div class="footer">- <a href="//www.bienvenidoainternet.org" target="_top">weabot</a> <?py include('templates/revision.html') ?> -</div>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/base_top.html b/cgi/templates/base_top.html
new file mode 100644
index 0000000..5389617
--- /dev/null
+++ b/cgi/templates/base_top.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<?py if 'matome' in _context: ?>
+ <title>#{matome} - #{board_long}</title>
+<?py elif board: ?>
+ <title>#{board_long}</title>
+ <?py else: ?>
+ <title>#{title}</title>
+<?py #endif ?>
+ <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<?py if replythread and 'threads' in _context and 'preview' in _context: ?>
+ <meta property="og:site_name" content="Bienvenido a Internet IB" />
+ <meta property="twitter:site" content="Bienvenido a Internet IB" />
+ <meta name="description" content="${preview}" />
+ <meta property="og:title" content="${threads[0]['posts'][0]['subject']} - ${board_name}" />
+ <meta property="og:description" content="${preview}" />
+ <?py if threads[0]['posts'][0]['thumb']: ?>
+ <meta property="twitter:image" content="https://bienvenidoainternet.org/#{board}/thumb/#{threads[0]['posts'][0]['thumb']}" />
+ <meta property="og:image" content="https://bienvenidoainternet.org/#{board}/thumb/#{threads[0]['posts'][0]['thumb']}" />
+ <?py #endif ?>
+ <meta property="twitter:title" content="${threads[0]['posts'][0]['subject']} - ${board_name}" />
+ <meta name="twitter:description" content="${preview}" />
+<?py #endif ?>
+ <meta name="robots" content="#{"noindex" if noindex else "index, follow"}" />
+ <link rel="shortcut icon" href="/favicon.ico" />
+ <link rel="stylesheet" href="#{static_url}css/ib.css" />
+<?py if not force_css: ?>
+ <link rel="stylesheet" id="css" href="#{static_url}css/#{styles[styles_default].lower()}.css" />
+<?py else: ?>
+ <link rel="stylesheet" type="text/css" href="#{force_css}" />
+<?py #endif ?>
+<?py if board == "2d": ?>
+ <link rel="stylesheet" href="#{static_url}css/txt/sjis.css" />
+<?py #endif ?>
+ <script type="text/javascript" src="#{static_url}js/weabot.js?v=5"></script>
+ <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script>
+ <script type="text/javascript" src="#{static_url}js/autorefresh.js?v=3"></script>
+</head>
+<body#{' class="res"' if replythread else ''}>
+ <div id="main_nav">[<a href="/" target="_top">Bienvenido a Internet</a>] [<?py include('templates/navbar.html') ?>]
+ <?py if not force_css: ?>
+ <span>[<span>Apariencia:</span>
+ <?py for title in styles: ?> <a href="#" class="ss">#{title}</a><?py #endfor ?>]</span>
+ <?py #endif ?></div>
+ <div class="logo">
+ <?py if board: ?>
+ #{board_long}
+ <?py else: ?>
+ <img src="/static/img/default.png" width="500" height="81" />
+ <?py #endif ?>
+ </div>
+ <hr width="90%" size="1" />
diff --git a/cgi/templates/board.0.html b/cgi/templates/board.0.html
new file mode 100644
index 0000000..1557cbc
--- /dev/null
+++ b/cgi/templates/board.0.html
@@ -0,0 +1,230 @@
+<?py include('templates/base_top.html') ?>
+<?py if replythread or oek_finish: ?>
+ &#91;<a href="#{boards_url}#{board}/">Volver al IB</a>&#93;
+<?py #endif ?>
+<?py if replythread: ?>
+ &#91;<a href="/cgi/catalog/${board}">Catálogo</a>&#93;
+ &#91;<a href="#bottom" name="top">Bajar</a>&#93;
+ <div class="replymode">Modo Respuesta</div>
+<?py #endif ?>
+<a name="postbox"></a>
+<div class="postarea">
+<?py if allow_oekaki and not oek_finish: ?>
+ <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <?py #endif ?>
+ Usar: <select name="oek_applet">
+ <option value="neo">PaintBBS NEO</option>
+ <option value="tegaki">Tegaki</option>
+ <option value="wpaint">wPaint</option>
+ <option value="shipainter|n|n">Shi-Painter</option>
+ <option value="shipainter|y|n">Shi-Painter Pro</option>
+ </select>
+ <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span>
+ <input type="submit" value="Dibujar" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a>
+ </form></center>
+<?py #endif ?>
+<?py if oek_finish: ?>
+<center style="margin-bottom:0.5em;"><table border=""><tr><td>
+ <?py if oek_finish == "no": ?>
+ <font size="+3">No hay dibujo</font>
+ <?py else: ?>
+ <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" />
+ <?py #endif ?>
+</td></tr></table></center>
+<?py #endif ?>
+<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <input type="hidden" name="default_subject" value="#{default_subject}" />
+ <?py #endif ?>
+ <div style="display:none;"><input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div>
+ <table class="postform">
+ <tr>
+ <td class="postblock">mediumo</td>
+ <td>
+ <input type="text" name="fieldb" size="25" accesskey="e" />
+ <?py if disable_subject: ?>
+ <?py if replythread: ?>
+ <input type="submit" value="🤡" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="🤡" accesskey="z" />
+ <?py #endif ?>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py if not disable_subject: ?>
+ <tr>
+ <td class="postblock">Asunto</td>
+ <td>
+ <input type="text" name="subject" size="35" maxlength="100" accesskey="s" />
+ <?py if replythread: ?>
+ <input type="submit" value="Responder" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="Crear hilo" accesskey="z" />
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">molekuloj</td>
+ <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td>
+ </tr>
+ <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?>
+ <tr>
+ <td class="postblock">amiko</td>
+ <td>
+ <input type="file" name="file" id="file" accesskey="f" />
+ <span id="filepreview" style="display:none;"></span>
+ <?py if allow_spoilers: ?>
+ <label>[<input type="checkbox" name="spoil" id="spoil" />Spoiler]</label>
+ <?py #endif ?>
+ <?py if allow_noimage and not replythread: ?>
+ <label>[<input type="checkbox" name="noimage" id="noimage" />Sin imagen]</label>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <tr class="pass">
+ <td class="postblock">timo</td>
+ <td><input type="password" name="password" size="8" accesskey="p" /> (uzata por post forigo)</td>
+ </tr>
+ <tr>
+ <td colspan="2" class="rules">
+ <ul>
+ #{postarea_desc}
+ <li>ni ne vivas timi, ni vivas konekti.</li>
+ <?py if supported_filetypes: ?>
+ <li>elekti la veneno: <span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. Äis: <span id="maxsize">#{maxsize}</span>KB. paÅo: #{maxdimensions}x#{maxdimensions}px</li>
+ <?py #endif ?>
+ </ul>
+ </td>
+ </tr>
+ </table>
+</form>
+</div>
+<hr />
+<?py if postarea_extra: ?>
+<center>#{postarea_extra}</center>
+<hr />
+<?py #endif ?>
+<form id="delform" action="#{cgi_url}delete" method="post">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if threads: ?>
+ <?py for thread in threads: ?>
+ <?py if not replythread: ?>
+ <span id="unhide#{thread['id']}#{board}" style="display:none;">Hilo <a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a> oculto. <a class="tt" href="#">Ver hilo</a></span>
+ <?py #endif ?>
+ <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}">
+ <?py for post in thread['posts']: ?>
+ <?py if int(post['parentid']) != 0: ?>
+ <table><tr><td class="ell">…</td>
+ <td class="reply" id="reply#{post['id']}">
+ <?py elif post['file']: ?>
+ <?py if post['image_width'] != '0': ?>
+ <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ <?py if not replythread: ?>
+ [<a href="#" title="Ocultar hilo" class="tt">Ocultar hilo</a>]
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <a name="#{post['id']}"></a>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <span class="deleted">No.#{post['id']} eliminado por usuario.</span>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <span class="deleted">No.#{post['id']} eliminado por miembro del staff.</span>
+ <?py else: ?>
+ <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" />
+ <?py if post['subject'] : ?>
+ <span class="subj">#{post['subject']}</span>
+ <?py #endif ?></label>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <span class="date" data-unix="#{random.randint(1,2147483647)}">#{post['timestamp_formatted']}</span>
+ <span class="reflink"><a>No.#{random.randint(1,999999)}</a></span>
+ <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a>
+ <?py if int(post['parentid']) != 0: ?>
+ <?py if post['file']: ?>
+ <div class="fs">
+ <?py if post['image_width'] != '0': ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if int(post['parentid']) == 0 and not replythread: ?>
+ [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">Responder</a>]
+ <?py if post['file'] == '': ?>
+ [<a href="#" title="Ocultar Hilo" class="tt">Ocultar</a>]
+ <?py #endif ?>
+ <?py #endif ?>
+ </div>
+ <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?>
+ <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;">
+ <?py else: ?>
+ <blockquote>
+ <?py #endif ?>
+ #{post['message']}
+ </blockquote>
+ <?py if not replythread and post['shortened']: ?>
+ <blockquote class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">aqu&iacute;</a> para verlo completo.)</blockquote>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if post['parentid'] == "0": ?>
+ <?py if not replythread: ?>
+ <?py if int(thread['omitted']) == 1: ?>
+ <div class="omitted">Un post omitido. Haz clic en Responder para ver.</div>
+ <?py elif int(thread['omitted']) > 1: ?>
+ <div class="omitted">#{thread['omitted']} posts omitidos. Haz clic en Responder para ver.</div>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py else: ?>
+ </td></tr></table>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <div class="cut"></div></div>
+ <?py if replythread: ?>
+ <hr />
+ <div class="nav">&#91;<a href="#{boards_url}#{board}/">Volver al IB</a>&#93;
+ &#91;<a href="/cgi/catalog/${board}">Catálogo</a>&#93;
+ &#91;<a href="#top" name="bottom">Subir</a>&#93;</div>
+ <?py #endif ?>
+ <hr />
+ <?py #endfor ?>
+ <div class="userdel">Eliminar post <label>[<input type="checkbox" name="imageonly" id="imageonly" />Sólo imagen]</label><br />
+ Clave <input type="password" name="password" size="8" /> <input name="deletepost" value="Eliminar" type="submit" /></div>
+ <?py #endif ?>
+</form>
+<?py if pagenav: ?>
+ <div class="pg">#{pagenav}</div>
+<?py #endif ?>
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/board.html b/cgi/templates/board.html
new file mode 100644
index 0000000..e91e187
--- /dev/null
+++ b/cgi/templates/board.html
@@ -0,0 +1,264 @@
+<?py include('templates/base_top.html') ?>
+<?py if replythread or oek_finish: ?>
+ &#91;<a href="#{boards_url}#{board}/">Volver al IB</a>&#93;
+<?py #endif ?>
+<?py if replythread: ?>
+ &#91;<a href="/cgi/catalog/${board}">Catálogo</a>&#93;
+ &#91;<a href="#bottom" name="top">Bajar</a>&#93;
+ <div class="replymode">Modo Respuesta</div>
+<?py #endif ?>
+<a name="postbox"></a>
+<div class="postarea">
+<?py if allow_oekaki and not oek_finish: ?>
+ <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <?py #endif ?>
+ Usar: <select name="oek_applet">
+ <option value="neo">PaintBBS NEO</option>
+ <option value="tegaki">Tegaki</option>
+ <option value="wpaint">wPaint</option>
+ <option value="shipainter|n|n">Shi-Painter</option>
+ <option value="shipainter|y|n">Shi-Painter Pro</option>
+ </select>
+ <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span>
+ <input type="submit" value="Dibujar" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a>
+ </form></center>
+<?py #endif ?>
+<?py if oek_finish: ?>
+<center style="margin-bottom:0.5em;"><table border=""><tr><td>
+ <?py if oek_finish == "no": ?>
+ <font size="+3">No hay dibujo</font>
+ <?py else: ?>
+ <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" />
+ <?py #endif ?>
+</td></tr></table></center>
+<?py #endif ?>
+<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <input type="hidden" name="default_subject" value="#{default_subject}" />
+ <?py #endif ?>
+ <div style="display:none;">Trampa: <input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div>
+ <table class="postform">
+ <?py if not disable_name: ?>
+ <tr>
+ <td class="postblock">Nombre</td>
+ <td><input type="text" name="fielda" size="25" accesskey="n" /></td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">E-mail</td>
+ <td>
+ <input type="text" name="fieldb" size="25" accesskey="e" />
+ <?py if disable_subject: ?>
+ <?py if replythread: ?>
+ <input type="submit" value="Responder" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="Crear hilo" accesskey="z" />
+ <?py #endif ?>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py if not disable_subject: ?>
+ <tr>
+ <td class="postblock">Asunto</td>
+ <td>
+ <input type="text" name="subject" size="35" maxlength="100" accesskey="s" />
+ <?py if replythread: ?>
+ <input type="submit" value="Responder" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="Crear hilo" accesskey="z" />
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">Mensaje</td>
+ <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td>
+ </tr>
+ <?py if not oek_finish: ?>
+ <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?>
+ <tr>
+ <td class="postblock">Archivo</td>
+ <td>
+ <input type="file" name="file" id="file" accesskey="f" />
+ <span id="filepreview" style="display:none;"></span>
+ <?py if allow_spoilers: ?>
+ <label>[<input type="checkbox" name="spoil" id="spoil" />Spoiler]</label>
+ <?py #endif ?>
+ <?py if allow_noimage and not replythread: ?>
+ <label>[<input type="checkbox" name="noimage" id="noimage" />Sin imagen]</label>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <?py elif oek_finish != 'no': ?>
+ <input type="hidden" name="oek_file" value="#{oek_finish}" />
+ <?py #endif ?>
+ <tr class="pass">
+ <td class="postblock">Clave</td>
+ <td><input type="password" name="password" size="8" accesskey="p" /> (para eliminar el post)</td>
+ </tr>
+ <tr>
+ <td colspan="2" class="rules">
+ <ul>
+ #{postarea_desc}
+ <?py if supported_filetypes: ?>
+ <li>Archivos permitidos: <span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. Hasta <span id="maxsize">#{maxsize}</span>KB. Miniaturas: #{maxdimensions}x#{maxdimensions}px</li>
+ <?py #endif ?>
+ <?py if not replythread: ?>
+ <li><a href="/cgi/catalog/${board}">Catálogo de hilos</a> (Orden: <a href="/cgi/catalog/${board}?sort=1">Nuevo</a>/<a href="/cgi/catalog/${board}?sort=2">Viejo</a>/<a href="/cgi/catalog/${board}?sort=3">Más</a>/<a href="/cgi/catalog/${board}?sort=4">Menos</a>)</li>
+ <?py #endif ?>
+ <?py if int(maxage) != 0: ?>
+ <li>Los hilos son automáticamente eliminados a los <b>#{maxage}</b> días de edad.</li>
+ <?py #endif ?>
+ <li>¿Eres nuevo? <a href="/guia.html">Leer antes de postear</a> · <a href="/faq.html">Preguntas frecuentes</a> · <a href="/bai/">Contacto</a></li>
+ </ul>
+ </td>
+ </tr>
+ </table>
+</form>
+</div>
+<hr />
+<?py if postarea_extra: ?>
+<center>#{postarea_extra}</center>
+<hr />
+<?py #endif ?>
+<form id="delform" action="#{cgi_url}delete" method="post">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if threads: ?>
+ <?py for thread in threads: ?>
+ <?py if not replythread: ?>
+ <span id="unhide#{thread['id']}#{board}" style="display:none;">Hilo <a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a> oculto. <a class="tt" href="#">Ver hilo</a></span>
+ <?py #endif ?>
+ <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}">
+ <?py for post in thread['posts']: ?>
+ <?py if int(post['parentid']) != 0: ?>
+ <table><tr><td class="ell">…</td>
+ <td class="reply" id="reply#{post['id']}">
+ <?py elif post['file']: ?>
+ <?py if post['image_width'] != '0': ?>
+ <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <div class="fs"><span>Nombre de archivo:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ <?py if not replythread: ?>
+ [<a href="#" title="Ocultar hilo" class="tt">Ocultar hilo</a>]
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <a name="#{post['id']}"></a>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <span class="deleted">No.#{post['id']} eliminado por usuario.</span>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <span class="deleted">No.#{post['id']} eliminado por miembro del staff.</span>
+ <?py else: ?>
+ <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" />
+ <?py if post['subject'] : ?>
+ <span class="subj">#{post['subject']}</span>
+ <?py #endif ?></label>
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?>
+ <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span>
+ <?py if replythread: ?>
+ <span class="reflink"><a href="##{post['id']}">No.</a><a href="#" class="postid">#{post['id']}</a></span>
+ <?py else: ?>
+ <span class="reflink"><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html##{post['id']}">No.</a><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html#i#{post['id']}">#{post['id']}</a></span>
+ <?py #endif ?>
+ <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a>
+ <?py if int(post['expires']): ?>
+ <small>Expira el ${post['expires_formatted']}</small>
+ <?py #endif ?>
+ <?py if int(post['parentid']) != 0: ?>
+ <?py if post['file']: ?>
+ <div class="fs">
+ <?py if post['image_width'] != '0': ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if int(post['parentid']) == 0 and not replythread: ?>
+ [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">Responder</a>]
+ <?py if post['file'] == '': ?>
+ [<a href="#" title="Ocultar Hilo" class="tt">Ocultar</a>]
+ <?py #endif ?>
+ <?py #endif ?>
+ </div>
+ <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?>
+ <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;">
+ <?py else: ?>
+ <blockquote>
+ <?py #endif ?>
+ #{post['message']}
+ </blockquote>
+ <?py if not replythread and post['shortened']: ?>
+ <blockquote class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">aqu&iacute;</a> para verlo completo.)</blockquote>
+ <?py #endif ?>
+ <?py if int(post['expires_alert']): ?>
+ <div style="color:red;font-weight:bold;">Este hilo es viejo y desaparecerá pronto.</div>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if post['parentid'] == "0": ?>
+ <?py if not replythread: ?>
+ <?py if int(thread['omitted']) == 1: ?>
+ <div class="omitted">Un post omitido. Haz clic en Responder para ver.</div>
+ <?py elif int(thread['omitted']) > 1: ?>
+ <div class="omitted">#{thread['omitted']} posts omitidos. Haz clic en Responder para ver.</div>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py else: ?>
+ </td></tr></table>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <div class="cut"></div></div>
+ <?py if replythread: ?>
+ <hr />
+ <div class="nav">&#91;<a href="#{boards_url}#{board}/">Volver al IB</a>&#93;
+ &#91;<a href="/cgi/catalog/${board}">Catálogo</a>&#93;
+ &#91;<a href="#top" name="bottom">Subir</a>&#93;</div>
+ <?py #endif ?>
+ <hr />
+ <?py #endfor ?>
+ <div class="userdel">Eliminar post <label>[<input type="checkbox" name="imageonly" id="imageonly" />Sólo imagen]</label><br />
+ Clave <input type="password" name="password" size="8" /> <input name="deletepost" value="Eliminar" type="submit" /></div>
+ <?py #endif ?>
+</form>
+<?py if pagenav: ?>
+ <div class="pg">#{pagenav}</div>
+<?py #endif ?>
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/board.jp.html b/cgi/templates/board.jp.html
new file mode 100644
index 0000000..8045ab1
--- /dev/null
+++ b/cgi/templates/board.jp.html
@@ -0,0 +1,271 @@
+<?py include('templates/base_top.html') ?>
+<?py if replythread or oek_finish: ?>
+ &#91;<a href="#{boards_url}#{board}/">掲示æ¿ã«æˆ»ã‚‹</a>&#93;
+<?py #endif ?>
+<?py if replythread: ?>
+ &#91;<a href="/cgi/catalog/${board}">カタログ</a>&#93;
+ &#91;<a href="#bottom" name="top">ボトムã¸è¡Œã</a>&#93;
+ <div class="replymode">レスé€ä¿¡ãƒ¢ãƒ¼ãƒ‰</div>
+<?py #endif ?>
+<a name="postbox"></a>
+<div class="postarea">
+<?py if allow_oekaki and not oek_finish: ?>
+ <center><form class="oekform" action="#{cgi_url}oekaki/paint" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <?py #endif ?>
+ <select name="oek_applet">
+ <option value="neo">PaintBBS NEO</option>
+ <option value="tegaki">Tegaki</option>
+ <option value="wpaint">wPaint</option>
+ <option value="shipainter|n|n">Shi-Painter</option>
+ <option value="shipainter|y|n">Shi-Painter Pro</option>
+ </select>
+ <span id="oek_size"><input type="text" name="oek_x" size="4" maxlength="4" value="300" /> x <input type="text" name="oek_y" size="4" maxlength="4" value="300" /></span>
+ <input type="submit" value="ãŠçµµã‹ãã™ã‚‹" /><br /><a href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">アップロード途中ã®ç”»åƒ</a>
+ </form></center>
+<?py #endif ?>
+<?py if oek_finish: ?>
+<center style="margin-bottom:0.5em;"><table border=""><tr><td>
+ <?py if oek_finish == "no": ?>
+ <font size="+3">ç”»åƒãŒè¦‹å½“ãŸã‚Šã¾ã›ã‚“</font>
+ <?py else: ?>
+ <img src="#{boards_url}oek_temp/#{oek_finish}.png?ts=#{ts}" />
+ <?py #endif ?>
+</td></tr></table></center>
+<?py #endif ?>
+<form name="postform" id="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if replythread: ?>
+ <input type="hidden" name="parent" value="#{replythread}" />
+ <input type="hidden" name="default_subject" value="#{default_subject}" />
+ <?py #endif ?>
+ <div style="display:none;">Trampa: <input type="text" name="name" size="25" /> <input type="text" name="email" size="25" /></div>
+ <table class="postform">
+ <?py if not disable_name: ?>
+ <tr>
+ <td class="postblock">ãŠãªã¾ãˆ</td>
+ <td><input type="text" name="fielda" size="25" accesskey="n" /></td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">E-mail</td>
+ <td>
+ <input type="text" name="fieldb" size="25" accesskey="e" />
+ <?py if disable_subject: ?>
+ <?py if replythread: ?>
+ <input type="submit" value="返信" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="スレッドを立ã¦ã‚‹" accesskey="z" />
+ <?py #endif ?>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py if not disable_subject: ?>
+ <tr>
+ <td class="postblock">題  å</td>
+ <td>
+ <input type="text" name="subject" size="35" maxlength="100" accesskey="s" />
+ <?py if replythread: ?>
+ <input type="submit" value="返信" accesskey="z" />
+ <?py else: ?>
+ <input type="submit" value="スレッドを立ã¦ã‚‹" accesskey="z" />
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">コメント</td>
+ <td><textarea name="message" cols="50" rows="6" accesskey="m"></textarea></td>
+ </tr>
+ <?py if not oek_finish: ?>
+ <?py if (replythread and allow_image_replies) or (not replythread and allow_images): ?>
+ <tr>
+ <td class="postblock">添付File</td>
+ <td>
+ <input type="file" name="file" id="file" accesskey="f" />
+ <span id="filepreview" style="display:none;"></span>
+ <?py if allow_spoilers: ?>
+ <label>[<input type="checkbox" name="spoil" id="spoil" />ãƒã‚¿ãƒãƒ¬]</label>
+ <?py #endif ?>
+ <?py if allow_noimage and not replythread: ?>
+ <label>[<input type="checkbox" name="noimage" id="noimage" />ç”»åƒãªã—]</label>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endif ?>
+ <?py elif oek_finish != 'no': ?>
+ <input type="hidden" name="oek_file" value="#{oek_finish}" />
+ <?py #endif ?>
+ <tr class="pass">
+ <td class="postblock">削除キー</td>
+ <td><input type="password" name="password" size="8" accesskey="p" /> (削除用)</td>
+ </tr>
+ <tr>
+ <td colspan="2" class="rules">
+ <ul>
+ #{postarea_desc}
+ <?py if supported_filetypes: ?>
+ <li>添付å¯èƒ½ï¼š<span id="filetypes">#{', '.join(supported_filetypes).upper()}</span>. <span id="maxsize">#{maxsize}</span>KBã¾ã§. #{maxdimensions}x#{maxdimensions}以上ã¯ç¸®å°.</li>
+ <?py #endif ?>
+ <?py if not replythread: ?>
+ <li><a href="#{cgi_url}catalog/${board}">カタログ</a> (ソート:<a href="/cgi/catalog/${board}?sort=1">æ–°é †</a>/<a href="/cgi/catalog/${board}?sort=2">å¤é †</a>/<a href="/cgi/catalog/${board}?sort=3">多順</a>/<a href="/cgi/catalog/${board}?sort=4">å°‘é †</a>)</li>
+ <?py #endif ?>
+ <?py if int(maxage) != 0: ?>
+ <li>スレã¯<b>#{maxage}</b>日間経ã¤ã¨è‡ªå‹•çš„ã«æ¶ˆã•ã‚Œã‚‰ã‚Œã¾ã™.</li>
+ <?py #endif ?>
+ <li><a href="/guia.html">使ã„æ–¹</a> · <a href="/faq.html">よãã‚る質å•</a> · <a href="/bai/">管ç†äººã¸ã®é€£çµ¡</a></li>
+ </ul>
+ </td>
+ </tr>
+ </table>
+</form>
+</div>
+<hr />
+<?py if postarea_extra: ?>
+<center>#{postarea_extra}</center>
+<hr />
+<?py #endif ?>
+<form id="delform" action="#{cgi_url}delete" method="post">
+ <input type="hidden" name="board" value="#{board}" />
+ <?py if threads: ?>
+ <?py for thread in threads: ?>
+ <?py if not replythread: ?>
+ <span id="unhide#{thread['id']}#{board}" style="display:none">スレ<a href="#{boards_url}#{board}/res/#{thread['id']}.html">#{thread['id']}</a>ã¯éš ã—ã¾ã—ãŸ. <a class="tt" href="#">スレを表示</a></span>
+ <?py #endif ?>
+ <div id="thread#{thread['id']}#{board}" class="thread" data-length="#{thread['length']}">
+ <?py for post in thread['posts']: ?>
+ <?py if int(post['parentid']) != 0: ?>
+ <table><tr><td class="ell">…</td>
+ <td class="reply" id="reply#{post['id']}">
+ <?py elif post['file']: ?>
+ <?py if post['image_width'] != '0': ?>
+ <div class="fs"><span>ç”»åƒãƒ•ã‚¡ã‚¤ãƒ«å:</span><a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <div class="fs"><span>ç”»åƒãƒ•ã‚¡ã‚¤ãƒ«å:</span><a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ <?py if post['file'][-3:] == 'gif': ?>
+ <small>アニメGIF</small>
+ <?py elif not post['thumb'].startswith('mime'): ?>
+ <small>サムãƒè¡¨ç¤º</small>
+ <?py #endif ?>
+ <?py if not replythread: ?>
+ [<a href="#" title="スレを隠ã™" class="tt">éš ã™</a>]
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <a name="#{post['id']}"></a>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <span class="deleted">No.#{post['id']}ã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«å‰Šé™¤ã•ã‚Œã¾ã—ãŸ.</span>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <span class="deleted">No.#{post['id']}ã¯ç®¡ç†äººã«å‰Šé™¤ã•ã‚Œã¾ã—ãŸ.</span>
+ <?py else: ?>
+ <div class="info"><label><input type="checkbox" name="delete" value="#{post['id']}" /><span class="subj">#{post['subject'] if post['subject'] else default_subject}</span></label>
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ Name <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ Name <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ Name <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ Name <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?>
+ <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span>
+ <?py if replythread: ?>
+ <span class="reflink"><a href="##{post['id']}">No.</a><a href="#" class="postid">#{post['id']}</a></span>
+ <?py else: ?>
+ <span class="reflink"><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html##{post['id']}">No.</a><a href="#{boards_url}#{board}/res/#{post['parentid'] if post['parentid'] != "0" else post['id']}.html#i#{post['id']}">#{post['id']}</a></span>
+ <?py #endif ?>
+ <a class="rep" href="#{cgi_url}report/#{board}/#{post['id']}" rel="nofollow">rep</a>
+ <?py if int(post['expires']): ?>
+ <small>${post['expires_formatted']}頃消ãˆã¾ã™</small>
+ <?py #endif ?>
+ <?py if int(post['parentid']) != 0: ?>
+ <?py if post['file']: ?>
+ <div class="fs">
+ <?py if post['image_width'] != '0': ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" class="expimg" target="_blank" data-id="#{post['id']}" data-thumb="#{images_url}#{board}/thumb/#{post['thumb']}" data-w="#{post['image_width']}" data-h="#{post['image_height']}" data-tw="#{post['thumb_width']}" data-th="#{post['thumb_height']}">#{post['file']}</a>-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']})
+ <?py else: ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank">#{post['file']}</a>-(#{post['file_size']} B)
+ <?py #endif ?>
+ <?py if post['file'][-3:] == 'gif': ?>
+ <small>アニメGIF</small>
+ <?py elif not post['thumb'].startswith('mime'): ?>
+ <small>サムãƒè¡¨ç¤º</small>
+ <?py #endif ?>
+ </div>
+ <a target="_blank" href="#{images_url}#{board}/src/#{post['file']}" id="thumb#{post['id']}">
+ <?py if post['thumb'].startswith('mime'): ?>
+ <img class="thumb" alt="#{post['id']}" src="/static/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py elif post['file'][-3:] == 'gif': ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/src/#{post['file']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py else: ?>
+ <img class="thumb" alt="#{post['id']}" src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" />
+ <?py #endif ?>
+ </a>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if int(post['parentid']) == 0 and not replythread: ?>
+ [<a href="#{boards_url}#{board}/res/#{post['id']}.html" class="hsbn">返信</a>]
+ <?py if post['file'] == '': ?>
+ [<a href="#" title="スレを隠ã™" class="tt">éš ã™</a>]
+ <?py #endif ?>
+ <?py #endif ?>
+ </div>
+ <?py if post['thumb_width'] != '0' and post['parentid'] != '0': ?>
+ <blockquote style="margin-left:#{int(post['thumb_width'])+40}px;">
+ <?py else: ?>
+ <blockquote>
+ <?py #endif ?>
+ #{post['message']}
+ </blockquote>
+ <?py if not replythread and post['shortened']: ?>
+ <blockquote class="abbrev">(投稿ã¯é•·ã™ãŽ... 全部読むã«ã¯<a href="#{boards_url}#{board}/res/#{post['id'] if post['parentid'] == "0" else post['parentid']}.html##{post['id']}">ã“ã£ã¡ã‚‰</a>ã¸)</blockquote>
+ <?py #endif ?>
+ <?py if int(post['expires_alert']): ?>
+ <div style="color:red;font-weight:bold">ã“ã®ã‚¹ãƒ¬ã¯å¤ã„ã®ã§ã€ã‚‚ã†ã™ã消ãˆã¾ã™ã€‚</div>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py if int(post['parentid']) == 0: ?>
+ <?py if not replythread: ?>
+ <?py if int(thread['omitted']) > 0: ?>
+ <span class="omitted">レス${thread['omitted']}件çœç•¥ã€‚å…¨ã¦èª­ã‚€ã«ã¯è¿”信ボタンを押ã—ã¦ãã ã•ã„。</span>
+ <?py #endif ?>
+ <?py #endif ?>
+ <?py else: ?>
+ </td></tr></table>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <div class="cut"></div></div>
+ <?py if replythread: ?>
+ <hr />
+ <div class="nav">&#91;<a href="#{boards_url}#{board}/">掲示æ¿ã«æˆ»ã‚‹</a>&#93;
+ &#91;<a href="/cgi/catalog/${board}">カタログ</a>&#93;
+ &#91;<a href="#top" name="bottom">トップã¸æˆ»ã‚‹</a>&#93;</div>
+ <?py #endif ?>
+ <hr />
+ <?py #endfor ?>
+ <div class="userdel">
+ ã€è¨˜äº‹å‰Šé™¤ã€‘<label>[<input type="checkbox" name="imageonly" id="imageonly" />ç”»åƒã ã‘消ã™]</label><br />
+ 削除キー<input type="password" name="password" size="8" /> <input name="deletepost" value="削除" type="submit" />
+ </div>
+ <?py #endif ?>
+</form>
+<?py if pagenav: ?>
+ <div class="pg">#{pagenav}</div>
+<?py #endif ?>
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/catalog.html b/cgi/templates/catalog.html
new file mode 100644
index 0000000..4faa2d2
--- /dev/null
+++ b/cgi/templates/catalog.html
@@ -0,0 +1,30 @@
+<?py include('templates/base_top.html') ?>
+<div id="ctrl">
+ &#91;<a href="#{boards_url}#{board}/">Volver al IB</a>&#93;
+ &#91;Orden:
+ <a class="cat_sort" data-sort="0" href="?sort=0">#{"<b>Normal</b>" if i_sort == "" else "Normal"}</a>
+ <a class="cat_sort" data-sort="1" href="?sort=1">#{"<b>Nuevo</b>" if i_sort == "1" else "Nuevo"}</a>
+ <a class="cat_sort" data-sort="2" href="?sort=2">#{"<b>Viejo</b>" if i_sort == "2" else "Viejo"}</a>
+ <a class="cat_sort" data-sort="3" href="?sort=3">#{"<b>Más</b>" if i_sort == "3" else "Más"}</a>
+ <a class="cat_sort" data-sort="4" href="?sort=4">#{"<b>Menos</b>" if i_sort == "4" else "Menos"}</a>&#93;
+ &#91;Tamaño: <a id="cat_size" href="#">Pequeño</a>&#93;
+ &#91;Texto: <a id="cat_hide" href="#">Ocultar</a>&#93;
+ &#91;Buscar: <input id="cat_search" type="text"><input type="hidden" name="board" value="#{board}" />
+</div>
+<div class="extramode">Modo Catálogo</div>
+<div id="catalog" style="margin:1em auto;">
+ <?py i = 1 ?>
+ <?py for thread in threads: ?><div id="cat#{thread['id']}#{board}" class="thread" data-num="${i}" data-id="#{thread['id']}" data-res="${thread['length']}">
+ <?py if thread['thumb'] != '': ?>
+ <a href="#{boards_url}#{board}/res/#{thread['id']}.html" rel="nofollow"><img src="#{images_url}#{board}/cat/#{thread['thumb']}" alt="#{thread['id']}" /></a><br />
+ <?py #endif ?>
+ <div class="replies">Respuestas: ${thread['length']}</div>
+ <?py if thread['thumb'] != '': ?>
+ <p><span class="subj">${thread['subject']}</span><br />${thread['message']}</p>
+ <?py else: ?>
+ <p><a href="#{boards_url}#{board}/res/#{thread['id']}.html" rel="nofollow" class="subj">${thread['subject']}</a><br />${thread['message']}</p>
+ <?py #endif ?>
+ <?py i += 1 ?>
+ </div><?py #endfor ?>
+</div>
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/error.html b/cgi/templates/error.html
new file mode 100644
index 0000000..47ef529
--- /dev/null
+++ b/cgi/templates/error.html
@@ -0,0 +1,7 @@
+<?py include('templates/base_top.html') ?>
+<br /><br /><hr size="1">
+<br /><br /><div style="text-align:center;color:red;font-size:x-large;font-weight:bold;">#{error}
+<br /><br /><a href="#{boards_url}#{board}/">Volver</a></div>
+<br /><br /><hr size="1">
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/exception.html b/cgi/templates/exception.html
new file mode 100644
index 0000000..e8453eb
--- /dev/null
+++ b/cgi/templates/exception.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Error@Bienvenido a Internet</title>
+<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<style type="text/css">.error{color:red;font-weight:bold;font-size:16pt} .sub{font-weight:bold}</style>
+</head>
+<body>
+<?py if exception: ?>
+<p class="error">ERROR : Ha ocurrido un error inesperado.</p>
+<p class="sub">Esto no es normal y te pedimos que reportes el problema en
+<a href="/bai/">Discusión de B.a.I.</a> o a través de
+<a href="mailto:burocracia@bienvenidoainternet.org">nuestro e-mail</a>,
+presentando los siguientes datos y ojalá indicando qué hacer para reproducirlo:</p>
+<p>Versión: weabot
+<?py include('templates/revision.html') ?><br />
+Tipo: ${exception}<br />
+Detalle: ${error}<br />
+Traceback:<br />
+<blockquote>
+ <?py for line in detail: ?>
+ ${line[0]} ${line[1]} ${line[2]} ${line[3]}<br />
+ <?py #endfor ?>
+</blockquote></p>
+<p class="sub">Te recordamos que el software está en desarrollo y estamos siempre haciendo lo posible para arreglar los problemas lo antes posible.<br />Te pedimos las disculpas por cualquier inconveniente.</p>
+<hr />
+<p>weabot dijo "Perdón."<br /><a href="/bai.html">Bienvenido a Internet BBS/IB</a></p>
+<?py else: ?>
+<p class="error">ERROR : #{error}</p>
+<p class="sub">Por favor presiona Atrás y soluciona el problema.</p>
+<hr />
+<p>La página principal está <a href="/bai.html">aquí</a>.<br />Si esto es inusual intenta <a href="/bai/">contactarnos</a>.</p><?py #endif ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/home.rss b/cgi/templates/home.rss
new file mode 100644
index 0000000..dc69377
--- /dev/null
+++ b/cgi/templates/home.rss
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0">
+ <channel>
+ <title>Bienvenido a Internet BBS/IB</title>
+ <link>https://bienvenidoainternet.org/</link>
+ <description>El BBS/IB más activo de la esfera hispana.</description>
+ <language>es</language>
+ <webMaster>burocracia@bienvenidoainternet.org (Staff ★)</webMaster>
+ <image>
+ <url>https://bienvenidoainternet.org/rss_logo.png</url>
+ <title>Bienvenido a Internet BBS/IB</title>
+ <link>https://bienvenidoainternet.org/</link>
+ <width>144</width>
+ <height>144</height>
+ </image>
+<?py for post in posts: ?>
+ <item>
+ <title>${post['board_name']}: #{post['content']}</title>
+ <pubDate>${post['timestamp_formatted']}</pubDate>
+ <link>https://bienvenidoainternet.org#{post['url']}</link>
+ </item>
+<?py #endfor ?>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/cgi/templates/htaccess b/cgi/templates/htaccess
new file mode 100644
index 0000000..469fec0
--- /dev/null
+++ b/cgi/templates/htaccess
@@ -0,0 +1,24 @@
+DirectoryIndex index.html
+<?py if dir == 'clusterfuck': ?>
+
+AuthName "BAI"
+AuthType Basic
+AuthUserFile "/home/z411/.htpasswds/public_html/wiki/passwd"
+<Limit GET>
+require valid-user
+</Limit>
+
+<?py #endif ?>
+<?py if dir == 'anarkia': ?>
+ExpiresByType text/css "access plus 0 seconds"
+<?py #endif ?>
+
+ErrorDocument 403 https://bienvenidoainternet.org/cgi/banned/#{dir}
+<?py if ips: ?>
+
+order allow,deny
+ <?py for ip in ips: ?>
+deny from #{ip}
+ <?py #endfor ?>
+allow from all
+<?py #endif ?>
diff --git a/cgi/templates/kako.html b/cgi/templates/kako.html
new file mode 100644
index 0000000..49d95df
--- /dev/null
+++ b/cgi/templates/kako.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Archivo de #{board_name}@Bienvenido a Internet BBS</title>
+ <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" />
+ <meta name="robots" content="index, follow" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="shortcut icon" href="#{static_url}img/favicon.ico" />
+ <style type="text/css">
+ body {margin:8px}
+ h1 {margin:0 0 20px}
+ pre {margin:0}
+ .fake {color:#0000EE;text-decoration:underline;cursor:pointer}
+ .fake:active {color:#FF0000}
+ img {width:20px;height:22px;margin-right:4px}
+ td {text-align:left;vertical-align:bottom;padding-right:14px}
+ .r {text-align:right}
+ a:link, a:hover {color:#0000EE}
+ a:active {color:#FF0000}
+ a:visited {color:#551A8B}
+ </style>
+</head>
+<body>
+<h1>Ãndice de /#{board}/kako/</h1>
+<pre>
+ <table style="border-collapse:collapse;">
+ <tr>
+ <th><img src="/blank.png" /></th>
+ <td><span class="fake">Nombre</span></td>
+ <td><span class="fake">Tamaño</span></td>
+ <td><span class="fake">Descripción</span></td>
+ </tr>
+ <tr>
+ <td colspan="4" style="padding:0"><hr /></td>
+ </tr>
+ <tr>
+ <th><img src="/back.png" /></th>
+ <td><a href="/#{board}/">..</a></td>
+ <td class="r">-</td>
+ <td></td>
+ </tr>
+ <?py for thread in threads: ?>
+ <tr>
+ <th><img src="/text.png" /></th>
+ <td><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">${thread['timestamp']}.json</a></td>
+ <?py if int(thread['length']) > 1000: ?>
+ <td class="r">1KR</td>
+ <?py else: ?>
+ <td class="r">${thread['length']}R</td>
+ <?py #endif ?>
+ <td>${thread['subject']}</td>
+ </tr>
+ <?py #endfor ?>
+ </table>
+ <hr />
+</pre>
+<address>weabot/0.8.4 (CentOS) Servidor ubicado en bienvenidoainternet.org Puerto 443</address>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/manage/addboard.html b/cgi/templates/manage/addboard.html
new file mode 100644
index 0000000..71b3c31
--- /dev/null
+++ b/cgi/templates/manage/addboard.html
@@ -0,0 +1,21 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Nuevo board</div>
+<form action="#{cgi_url}manage/addboard" method="post">
+ <table>
+ <tr>
+ <td class="postblock">Directorio</td>
+ <td><input type="text" name="dir" maxlength="16" style="width:100%;" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Nombre</td>
+ <td><input type="text" name="name" maxlength="64" style="width:100%;" /></td>
+ </tr>
+ <tr>
+ <td colspan="2"><input type="submit" name="submit" style="width:100%;" value="Agregar board" /></td>
+ </table>
+</form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/bans.html b/cgi/templates/manage/bans.html
new file mode 100644
index 0000000..81e0f71
--- /dev/null
+++ b/cgi/templates/manage/bans.html
@@ -0,0 +1,92 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Bans</div>
+<?py if mode == 0: ?>
+<form action="#{cgi_url}manage/ban/" name="banform" method="post">
+<table>
+<tr>
+ <td class="postblock">Dirección IP</td>
+ <td><input type="text" name="ip" size="20" /></td>
+</tr>
+<tr><td colspan="2"><input type="submit" value="Ir a formulario de ban" style="width:100%;" /></td></tr>
+</table>
+</form>
+<hr />
+<table class="managertable">
+<tr>
+ <th>Dirección IP</th>
+ <th>Máscara de red</th>
+ <th>Boards</th>
+ <th>Agregado</th>
+ <th>Expira</th>
+ <th>Ciego</th>
+ <th>Puesto por</th>
+ <th>Razón</th>
+ <th>Nota</th>
+ <th>Acción</th>
+</tr>
+<?py for ban in bans: ?>
+<tr>
+ <td>${ban['ip']}</td>
+ <td>${ban['netmask']}</td>
+ <td>${ban['boards']}</td>
+ <td>${ban['added']}</td>
+ <td>${ban['until']}</td>
+ <td>${ban['blind']}</td>
+ <td>${ban['staff']}</td>
+ <td>${ban['reason']}</td>
+ <td>${ban['note']}</td>
+ <td>
+ [<a href="#{cgi_url}manage/ipshow?ip=#{ban['ip']}">Ver posts</a>]
+ [<a href="#{cgi_url}manage/ban?ip=#{ban['ip']}&amp;edit=#{ban['id']}">Editar</a>]
+ [<a href="#{cgi_url}manage/bans/delete/#{ban['id']}">Eliminar</a>]
+ </td>
+</tr>
+<?py #endfor ?>
+</table>
+<?py elif mode == 1: ?>
+<form action="#{cgi_url}manage/ban" name="banform" method="post">
+<table>
+ <tr><td class="postblock">IP</td><td><input type="text" name="ip" value="${ip}" size="20" style="width:100%;" /></td></tr>
+ <tr><td class="postblock">Máscara de red</td><td><input type="text" name="netmask" value="${startvalues['netmask']}" style="width:100%;" /></td></tr>
+ <tr>
+ <td class="postblock">Board(s)</td>
+ <td>
+ <input type="checkbox" name="board_all" id="b_all" value="1"#{checked(startvalues['where'] == '')} /><label for="b_all" style="font-weight:bold">Todos los boards</label><hr />
+ <?py for board in boards: ?>
+ <input type="checkbox" name="board_#{board['dir']}" id="b#{board['dir']}" value="1"#{checked(board['dir'] in startvalues['where'])} /><label for="b#{board['dir']}">${board['name']}</label><br />
+ <?py #endfor ?>
+ <?py if edit_id > 0: ?>
+ <input type="hidden" name="edit" value="${edit_id}" />
+ <?py #endif ?>
+ </td>
+ </tr>
+ <tr><td class="postblock">Mensaje</td><td><textarea name="reason" style="width:100%;">${startvalues['reason']}</textarea></td></tr>
+ <tr><td class="postblock">Nota para staff</td><td><input type="text" name="note" value="${startvalues['note']}" style="width:100%;" /></td></tr>
+ <tr><td class="postblock">Ciego</td><td><input type="checkbox" name="blind" id="blind" value="1"#{checked(startvalues['blind'] == '1')} /><label for="blind"></label></td></tr>
+ <tr><td class="postblock">Expira en <span style="font-weight:normal;">(segundos)</span></td><td><input type="text" id="seconds" name="seconds" value="#{startvalues['seconds']}" style="width:100%;" />
+ <br />
+ <div id="timelist">
+ <a href="#" data-secs="0">Nunca</a>
+ <a href="#" data-secs="3600">1h</a>
+ <a href="#" data-secs="21600">6h</a>
+ <a href="#" data-secs="43200">12h</a>
+ <a href="#" data-secs="86400">1d</a>
+ <a href="#" data-secs="259200">3d</a>
+ <a href="#" data-secs="604800">1w</a>
+ <a href="#" data-secs="2592000">30d</a>
+ <a href="#" data-secs="31536000">1y</a>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2"><input type="submit" value="Colocar ban" style="width:100%;" /></td>
+ </tr>
+</table>
+</form>
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/boardoptions.html b/cgi/templates/manage/boardoptions.html
new file mode 100644
index 0000000..436b036
--- /dev/null
+++ b/cgi/templates/manage/boardoptions.html
@@ -0,0 +1,195 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Opciones de Board</div>
+<?py if mode == 0: ?>
+<table class="managertable">
+ <tr><th colspan="2">Sección</th><th>Accion</th></tr>
+ <?py for board in boards: ?>
+ <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/board/#{board['dir']}">Configurar</a>]</td></tr>
+ <?py #endfor ?>
+ </table>
+<?py elif mode == 1: ?>
+<form action="#{cgi_url}manage/board/${boardopts['dir']}" method="post">
+<table>
+<tr>
+ <td class="postblock">ID</td>
+ <td><input type="text" name="name" value="${boardopts['id']}" maxlength="16" style="width:100%;" disabled="disabled" /></td>
+</tr>
+<tr>
+ <td class="postblock">Directorio</td>
+ <td><input type="text" name="name" value="${boardopts['dir']}" maxlength="32" style="width:100%;" disabled="disabled" /></td>
+</tr>
+<tr>
+ <td class="postblock">Nombre</td>
+ <td><input type="text" name="name" value="${boardopts['name']}" maxlength="64" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Nombre largo</td>
+<td><input type="text" name="longname" size="50" value="${boardopts['longname']}" maxlength="128" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Sub-nombre</td>
+<td><input type="text" name="subname" value="${boardopts['subname']}" maxlength="3" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Tipo</td>
+<td>
+ <select style="width:100%;" name="type">
+ <option value="0">Imageboard</option>
+ <option value="1"#{selected(boardopts['board_type'] == '1')}>Textboard</option>
+ </select>
+</td>
+</tr>
+<tr>
+<td class="postblock">Descripción / Reglas</td>
+<td>
+ <textarea id="brd_desc" name="postarea_desc" rows="10" cols="50" style="width:100%;">${boardopts['postarea_desc']}</textarea>
+ <div id="prev_desc" style="border:1px dotted gray;display:none;padding:4px;width:100%;" contenteditable="true"></div>
+</td>
+</tr>
+<tr>
+<td class="postblock">Caja extra</td>
+<td><textarea name="postarea_extra" rows="5" cols="50" style="width:100%;">${boardopts['postarea_extra']}</textarea></td>
+</tr>
+<tr>
+<td class="postblock">Forzar CSS <span style="font-weight:normal;">("" = default)</span></td>
+<td><input type="text" name="force_css" size="50" value="#{boardopts['force_css']}" maxlength="255" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Nombre por defecto</td>
+<td><input type="text" name="anonymous" size="50" maxlength="128" value="${boardopts['anonymous']}" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Título por defecto</td>
+<td><input type="text" name="subject" size="50" maxlength="64" value="${boardopts['subject']}" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">Mensaje por defecto</td>
+<td><input type="text" name="message" size="50" maxlength="128" value="${boardopts['message']}" style="width:100%;" /></td>
+</tr>
+<tr>
+<td class="postblock">ID</td>
+<td>
+ <select name="useid" style="width:100%;">
+ <option value="0">Desactivado</option>
+ <option value="1"#{selected(boardopts['useid'] == '1')}>Activado</option>
+ <option value="2"#{selected(boardopts['useid'] == '2')}>Activado siempre</option>
+ <option value="3"#{selected(boardopts['useid'] == '3')}>Activado siempre, detallado</option>
+ </select>
+</td>
+</tr>
+<tr>
+<td class="postblock">Slip</td>
+<td>
+ <select name="slip" style="width:100%;">
+ <option value="0">Desactivado</option>
+ <option value="1"#{selected(boardopts['slip'] == '1')}>Activado</option>
+ <option value="2"#{selected(boardopts['slip'] == '2')}>Sólo dominio</option>
+ <option value="3"#{selected(boardopts['slip'] == '3')}>Todo</option>
+ </select>
+</td>
+</tr>
+<tr>
+<td class="postblock">Código de país</td>
+<td>
+ <select name="countrycode" style="width:100%;">
+ <option value="0">Desactivado</option>
+ <option value="1"#{selected(boardopts['countrycode'] == '1')}>Activado</option>
+ </select>
+</td>
+</tr>
+<tr>
+<td class="postblock">Desactivar nombre</td>
+<td><input type="checkbox" name="disable_name" id="noname" value="1"#{checked(boardopts['disable_name'] == '1')} /><label for="noname"></label></td>
+</tr>
+<tr>
+<td class="postblock">Desactivar asunto</td>
+<td><input type="checkbox" name="disable_subject" id="nosub" value="1"#{checked(boardopts['disable_subject'] == '1')} /><label for="nosub"></label></td>
+</tr>
+<tr>
+<td class="postblock">Papelera de reciclaje</td>
+<td><input type="checkbox" name="recyclebin" id="bin" value="1"#{checked(boardopts['recyclebin'] == '1')} /><label for="bin"></label></td>
+</tr>
+<tr>
+<td class="postblock">Cerrado</td>
+<td><input type="checkbox" name="locked" id="locked" value="1"#{checked(boardopts['locked'] == '1')} /><label for="locked"></label></td>
+</tr>
+<tr>
+<td class="postblock">Secreto</td>
+<td><input type="checkbox" name="secret" id="secret" value="1"#{checked(boardopts['secret'] == '1')} /><label for="secret"></label></td>
+</tr>
+<tr>
+<td class="postblock">Permitir spoilers</td>
+<td><input type="checkbox" name="allow_spoilers" id="spoil" value="1"#{checked(boardopts['allow_spoilers'] == '1')} /><label for="spoil"></label></td>
+</tr>
+<tr>
+<td class="postblock">Permitir oekaki</td>
+<td><input type="checkbox" name="allow_oekaki" id="oek" value="1"#{checked(boardopts['allow_oekaki'] == '1')} /><label for="oek"></label></td>
+</tr>
+<tr>
+<td class="postblock">Permitir crear hilos sin imagen</td>
+<td><input type="checkbox" name="allow_noimage" id="noimgallow" value="1"#{checked(boardopts['allow_noimage'] == '1')} /><label for="noimgallow"></label></td>
+</tr>
+<tr>
+<td class="postblock">Permitir subida</td>
+<td><input type="checkbox" name="allow_images" id="img" value="1"#{checked(boardopts['allow_images'] == '1')} /><label for="img">Al crear un hilo</label><br /><input type="checkbox" name="allow_image_replies" id="imgres" value="1"#{checked(boardopts['allow_image_replies'] == '1')} /><label for="imgres">Al responder</label></td>
+</tr>
+<tr>
+<td class="postblock">Tipos de archivo</td>
+<td>
+ <?py for filetype in filetypes: ?>
+ <input type="checkbox" name="filetype#{filetype['ext']}" id="#{filetype['ext']}" value="1"#{checked(filetype['ext'] in supported_filetypes)} /><label for="#{filetype['ext']}">${filetype['ext'].upper()}</label><br />
+ <?py #endfor ?>
+</td>
+</tr>
+<tr>
+<td class="postblock">Tamaño máximo <span style="font-weight:normal;">(KB)</span></td>
+<td><input type="text" name="maxsize" value="#{boardopts['maxsize']}" maxlength="5" size="11" /></td>
+</tr>
+<tr>
+<td class="postblock">Dimensión de miniatura <span style="font-weight:normal;">(px)</span></td>
+<td><input type="text" name="thumb_px" value="#{boardopts['thumb_px']}" maxlength="3" size="11" /></td>
+</tr>
+<tr>
+<td class="postblock">Hilos en página frontal</td>
+<td><input type="text" name="numthreads" value="#{boardopts['numthreads']}" maxlength="2" size="11" /></td>
+</tr>
+<tr>
+<td class="postblock">Respuestas a mostrar</td>
+<td><input type="text" name="numcont" value="#{boardopts['numcont']}" maxlength="2" size="11" /></td>
+</tr>
+<tr>
+<td class="postblock">Máximo de líneas <span style="font-weight:normal;">(frontal)</span></td>
+<td><input type="text" name="numline" value="#{boardopts['numline']}" maxlength="3" size="11" /></td>
+</tr>
+<tr>
+<td class="postblock">Edad máxima de un hilo</td>
+<td><input type="text" name="maxage" value="#{boardopts['maxage']}" maxlength="3" size="11" /> (días; 0 = desactivar)</td>
+</tr>
+<tr>
+<td class="postblock">Inactividad máxima de un hilo</td>
+<td><input type="text" name="maxinactive" value="#{boardopts['maxinactive']}" maxlength="3" size="11" /> (días; 0 = desactivar)</td>
+</tr>
+<tr>
+<td class="postblock">Archivar hilos</td>
+<td><input type="checkbox" name="archive" id="arch" value="1"#{checked(boardopts['archive'] == '1')} /><label for="arch"></label></td>
+</tr>
+<tr>
+<td class="postblock">Espera para crear nuevo hilo</td>
+<td><input type="text" name="threadsecs" value="#{boardopts['threadsecs']}" maxlength="4" size="11" /> (segundos)</td>
+</tr>
+<tr>
+<td class="postblock">Espera entre respuestas</td>
+<td><input type="text" name="postsecs" value="#{boardopts['postsecs']}" maxlength="3" size="11" /> (segundos)</td>
+</tr>
+</table>
+<br />
+<hr />
+<input type="submit" value="Guardar cambios" />
+</form>
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/changepassword.html b/cgi/templates/manage/changepassword.html
new file mode 100644
index 0000000..977c772
--- /dev/null
+++ b/cgi/templates/manage/changepassword.html
@@ -0,0 +1,24 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Cambiar contraseña</div>
+<form action="#{cgi_url}manage/changepassword" method="post">
+<table>
+ <tr>
+ <td class="postblock">Clave actual</td>
+ <td><input type="password" name="oldpassword" style="width:100%;" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Nueva clave</td>
+ <td><input type="password" name="newpassword" style="width:100%;" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Confirmar nueva clave</td>
+ <td><input type="password" name="newpassword2" style="width:100%;" /></td>
+ </tr>
+ <tr><td colspan="2"><input type="submit" style="width:100%;" value="Cambiar" /></td></tr>
+</table>
+</form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/delete.html b/cgi/templates/manage/delete.html
new file mode 100644
index 0000000..78c1c5e
--- /dev/null
+++ b/cgi/templates/manage/delete.html
@@ -0,0 +1,23 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Eliminar Post</div>
+<form action="#{cgi_url}manage/delete_confirmed/#{curboard}/#{postid}" method="get">
+<?py if do_ban: ?>
+ <input type="hidden" name="ban" value="true" />
+<?py #endif ?>
+<p>
+ <b>Post #${postid} de /${curboard}/</b><br />
+ <input id="a" type="checkbox" name="imageonly" value="true" /><label for="a">Eliminar sólo archivo</label><br />
+ <input id="b" type="checkbox" name="perma" value="true" /><label for="b" style="font-weight:bold">Eliminar permanentemente</label><br />
+ <br />
+ <i>Nota: Por favor evitar eliminar <b>permanentemente</b> el post al menos que sea estrictamente necesario.
+ <br />Al eliminar permanentemente un post no queda en papelera y se rompen
+ las referencias que se pueden haber hecho hacia él, especialmente en los BBS.</i>
+ <br /><br />
+ <input type="submit" value="Eliminar" />
+</p>
+</form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/filters.html b/cgi/templates/manage/filters.html
new file mode 100644
index 0000000..188a741
--- /dev/null
+++ b/cgi/templates/manage/filters.html
@@ -0,0 +1,119 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Filtros</div>
+<?py if mode == 0: ?>
+<table class="managertable">
+ <tr>
+ <th>ID</th>
+ <th>Boards</th>
+ <th>Tipo</th>
+ <th>Acción</th>
+ <th>Mensaje</th>
+ <th>Modificado</th>
+ <th>Por</th>
+ <th>Acción</th>
+ </tr>
+ <?py for filter in filters: ?>
+ <tr>
+ <td style="text-align:center">#{filter['id']}</td>
+ <td style="text-align:center">${filter['boards']}</td>
+ <td>#{filter['type_formatted']}</td>
+ <td>#{filter['action_formatted']}</td>
+ <td>${filter['reason']}</td>
+ <td style="text-align:center">${filter['added']}</td>
+ <td style="text-align:center">${filter['staff']}</td>
+ <td style="text-align:center">[<a href="#{cgi_url}manage/filters/add?edit=#{filter['id']}">Editar</a>]<br />
+ [<a href="#{cgi_url}manage/filters/delete/#{filter['id']}">Eliminar</a>]</td>
+ </tr>
+ <?py #endfor ?>
+ <tr><td colspan="9" style="text-align:center">
+ <form action="#{cgi_url}manage/filters/add" method="get">
+ <input type="submit" value="Agregar filtro" />
+ </form></td>
+ </tr>
+</table>
+<?py elif mode == 1: ?>
+<form name="banform" method="post">
+<table>
+ <tr><th colspan="3" class="postblock">Tipo de filtro</th></tr>
+ <tr>
+ <td class="postblock"><input type="radio" name="type" id="type1" value="0"#{checked(startvalues['type'] == '0')} /><label for="type1">Palabra</label></td>
+ <td style="text-align:right">Regex:</td>
+ <td><input type="text" name="word" value="${startvalues['word']}" /></td>
+ </tr>
+ <tr>
+ <td rowspan="2" class="postblock"><input type="radio" name="type" id="type2" value="1"#{checked(startvalues['type'] == '1')} /><label for="type2">Nombre/Tripcode</label></td>
+ <td style="text-align:right">Nombre:</td>
+ <td><input type="text" name="name" value="${startvalues['name']}" /> (regex)</td>
+ </tr>
+ <tr>
+ <td style="text-align:right">Tripcode:</td>
+ <td><input type="text" name="trip" value="${startvalues['trip']}" /> (incluir separador)</td>
+ </tr>
+</table>
+<br />
+<div style="text-align:left;display:inline-block;">
+ <div class="postblock" style="display:block;text-align:center;margin-bottom:0.5em;">Aplicar a</div>
+ <div style="padding:0 10px">
+ <input type="checkbox" name="board_all" id="board_all" value="1"#{checked(startvalues['where'] == '')} /><label for="board_all" style="font-weight:bold">Todos los boards</label>
+ <hr />
+ <?py for board in boards: ?>
+ <input type="checkbox" name="board_#{board['dir']}" id="board_#{board['dir']}" value="1"#{checked(board['dir'] in startvalues['where'])} /><label for="board_#{board['dir']}">${board['name']} <span style="opacity:0.5">(/#{board['dir']}/)</span></label><br />
+ <?py #endfor ?>
+ </div>
+</div>
+<br /><br />
+<table>
+ <tr>
+ <th colspan="3" class="postblock">Acción</th>
+ </tr>
+ <tr>
+ <td class="postblock"><input type="radio" name="action" id="act0" value="0"#{checked(startvalues['action'] == '0')} /><label for="act0">Abortar post</label></td>
+ <td colspan="2"></td>
+ </tr>
+ <tr>
+ <td class="postblock"><input type="radio" name="action" id="act1" value="1"#{checked(startvalues['action'] == '1')} /><label for="act1">Reemplazar</label></td>
+ <td colspan="2"><input type="text" name="changeto" value="#{startvalues['changeto']}" size="40" /></td>
+ </tr>
+ <tr>
+ <td rowspan="2" class="postblock"><input type="radio" name="action" id="act2" value="2"#{checked(startvalues['action'] == '2')} /><label for="act2">Autoban</label></td>
+ <td style="text-align:right">Expira en:</td>
+ <td><input type="text" name="seconds" id="seconds" size="6" value="#{startvalues['seconds']}" /> (segundos)<div style="float:right"><input type="checkbox" name="blind" id="blind" value="1"#{checked(startvalues['blind'] == '1')} /><label for="blind">Ban ciego</label></div></td>
+ </tr>
+ <tr>
+ <td style="text-align:right">Preset:</td>
+ <td id="timelist">
+ <a href="#" data-secs="0">Nunca</a>
+ <a href="#" data-secs="3600">1h</a>
+ <a href="#" data-secs="21600">6h</a>
+ <a href="#" data-secs="43200">12h</a>
+ <a href="#" data-secs="86400">1d</a>
+ <a href="#" data-secs="259200">3d</a>
+ <a href="#" data-secs="604800">1w</a>
+ <a href="#" data-secs="2592000">30d</a>
+ <a href="#" data-secs="31536000">1y</a>
+ </td>
+ </tr>
+ <tr>
+ <td rowspan="2" class="postblock"><input type="radio" name="action" id="act3" value="3"#{checked(startvalues['action'] == '3')} /><label for="act3">Redireccionar</label></td>
+ <td colspan="2"><input type="text" name="redirect_url" value="#{startvalues['redirect_url']}" size="40" /></td>
+ </tr>
+ <tr>
+ <td style="text-align:right">Tardar:</td>
+ <td><input type="text" name="redirect_time" size="6" value="#{startvalues['redirect_time']}" /> (segundos)</td>
+ </tr>
+</table>
+<br />
+<table>
+ <tr><th class="postblock" style="padding:2px">Mensaje a mostrar</th></tr>
+ <tr><td><input type="text" size="50" name="reason" value="#{startvalues['reason']}" /></td></tr>
+</table>
+<br />
+<input type="submit" name="add" value="#{submit}" />
+</form>
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/ipdelete.html b/cgi/templates/manage/ipdelete.html
new file mode 100644
index 0000000..71c043a
--- /dev/null
+++ b/cgi/templates/manage/ipdelete.html
@@ -0,0 +1,24 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+ <div class="replymode">Eliminar por IP</div>
+ <form action="#{cgi_url}manage/ipdelete" name="ipdeleteform" method="post">
+ <table>
+ <tr>
+ <td class="postblock">Board(s)</td>
+ <td>
+ <input type="checkbox" name="board_all" id="all" value="1" /><label for="all" style="font-weight:bold">Todos los boards</label><hr />
+ <?py for board in boards: ?>
+ <input type="checkbox" name="board_#{board['dir']}" id="#{board['dir']}" value="1" /><label for="#{board['dir']}">#{board['name']} <span style="opacity:0.5">(/#{board['dir']}/)</span></label><br />
+ <?py #endfor ?>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Dirección IP</td>
+ <td><input type="text" name="ip" style="width:100%;" /></td>
+ </tr>
+ <tr><td colspan="2"><input type="submit" style="width:100%;" value="Eliminar posts" /></td></tr>
+ </table>
+ </form>
+</center><hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/ipshow.html b/cgi/templates/manage/ipshow.html
new file mode 100644
index 0000000..6937a0e
--- /dev/null
+++ b/cgi/templates/manage/ipshow.html
@@ -0,0 +1,73 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+ <div class="replymode">Mostrar por IP</div>
+ <?py if mode == 0: ?>
+ <form action="#{cgi_url}manage/ipshow" method="post">
+ <table>
+ <tr><td class="postblock">Dirección IP</td><td><input type="text" name="ip" /></td></tr>
+ <tr><td colspan="2"><input type="submit" style="width:100%;" value="Mostrar posts" /></td></tr>
+ </table>
+ </form>
+ <?py else: ?>
+ <style>td img{max-width:150px;height:auto;}td.z{padding:0}</style>
+ <div class="logo" style="margin:0;">Actividad IP #{ip} (#{len(posts)})</div>
+ <center>
+ Hostname: #{host if host else "Desconocido"} [#{country if country else "??"}]#{" (Nodo Tor)" if tor else ""}<br />
+ <br />
+ <form action="#{cgi_url}manage/ban/" name="banform" method="post"><input type="hidden" name="ip" value="${ip}" /><input type="submit" value="Ir a formulario de ban" /></form>
+ <hr />
+ <?py if posts: ?>
+ <table class="managertable">
+ <tr>
+ <th>Sección</th>
+ <th>Padre</th>
+ <th>ID</th>
+ <th>Fecha</th>
+ <th>Nombre</th>
+ <th>Asunto</th>
+ <th>Mensaje</th>
+ <th>Archivo</th>
+ <th>Acción</th>
+ </tr>
+ <?py for post in posts: ?>
+ <tr>
+ <td>#{post['dir']}</td>
+ <td>#{post['parentid']}</td>
+ <td>#{post['id']}</td>
+ <td class="date" data-unix="${post['timestamp']}">#{post['timestamp_formatted']}</td>
+ <?py if post['tripcode']: ?>
+ <td class="name"><b>#{post['name']}</b> #{post['tripcode']}</td>
+ <?py else: ?>
+ <td class="name"><b>#{post['name']}</b></td>
+ <?py #endif ?>
+ <td>#{post['subject']}</td>
+ <td>#{post['message']}</td>
+ <?py if post['file']: ?>
+ <td class="z"><img src="#{images_url}#{post['dir']}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /></td>
+ <?py else: ?>
+ <td></td>
+ <?py #endif ?>
+ <td>
+ <?py if post['IS_DELETED'] == '0': ?>
+ <a href="#{cgi_url}manage/delete/#{post['dir']}/#{post['id']}">Eliminar</a>
+ <?py elif post['IS_DELETED'] == '1': ?>
+ <a href="#{cgi_url}manage/recyclebin/0/restore/#{post['dir']}/#{post['id']}">Rec</a>
+ <abbr title="Eliminado por usuario">[1]</abbr>
+ <?py else: ?>
+ <a href="#{cgi_url}manage/recyclebin/0/restore/#{post['dir']}/#{post['id']}">Rec</a>
+ <abbr title="Eliminado por staff">[2]</abbr>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <?py #endfor ?>
+ </table>
+ <hr />
+ <?py else: ?>
+ <b>Error:</b> No hay posts<br /><br />
+ <?py #endif ?>
+ [<a href="#{cgi_url}manage/ipshow">Volver al panel</a>]
+ <?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/lockboard.html b/cgi/templates/manage/lockboard.html
new file mode 100644
index 0000000..cebf061
--- /dev/null
+++ b/cgi/templates/manage/lockboard.html
@@ -0,0 +1,20 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Cerrar o abrir board</div>
+<table class="managertable">
+ <tr><th colspan="2">Sección</th><th>Acción</th></tr>
+ <?py for board in boards: ?>
+ <tr>
+ <td>/#{board['dir']}/</td><td>#{board['name']}</td>
+ <?py if board['locked'] == '0': ?>
+ <td style="text-align:center;">[<a href="#{cgi_url}manage/boardlock/#{board['dir']}">Cerrar</a>]</td>
+ <?py elif board['locked'] == '1': ?>
+ <td style="text-align:center;">[<a href="#{cgi_url}manage/boardlock/#{board['dir']}">Abrir</a>]</td>
+ <?py #endif ?>
+ </tr>
+ <?py #endfor ?>
+</table>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/login.html b/cgi/templates/manage/login.html
new file mode 100644
index 0000000..7ce47a1
--- /dev/null
+++ b/cgi/templates/manage/login.html
@@ -0,0 +1,21 @@
+<?py include('templates/base_top.html') ?>
+<center>
+ #{page}
+ <form action="#{cgi_url}manage" method="post">
+ <table>
+ <tr>
+ <td class="postblock">Usuario</td>
+ <td><input type="text" name="username" /></td>
+ </tr>
+ <tr>
+ <td class="postblock">Contraseña</td>
+ <td><input type="password" name="password" /></td>
+ </tr>
+ <tr>
+ <td colspan="2"><input id="submit" type="submit" name="submit" style="width:100%;" value="Entrar" /></td>
+ </tr>
+ </table>
+ </form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/logs.html b/cgi/templates/manage/logs.html
new file mode 100644
index 0000000..e11780a
--- /dev/null
+++ b/cgi/templates/manage/logs.html
@@ -0,0 +1,17 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Registro</div>
+<table class="managertable">
+ <tr><th>Fecha</th><th>Staff</th><th>Acción</th></tr>
+<?py for log in logs: ?>
+ <tr>
+ <td class="date" data-unix="${log['timestamp']}" style="white-space:nowrap;">${log['timestamp_formatted']}</td>
+ <td>${log['staff']}</td>
+ <td>${log['action']}</td>
+ </tr>
+<?py #endfor ?>
+</table>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/manage.html b/cgi/templates/manage/manage.html
new file mode 100644
index 0000000..06b1737
--- /dev/null
+++ b/cgi/templates/manage/manage.html
@@ -0,0 +1,22 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+ <div style="margin:0.5em 0;"><strong>BANDEJA DE ENTRADA</strong>
+ <br />
+ Denuncias:
+ <?py if int(reports) > 0: ?>
+ <a href="#{cgi_url}manage/reports" style="color:red;font-weight:bold;">#{reports}</a>
+ <?py else: ?>
+ 0
+ <?py #endif ?></div>
+ <hr />
+ <strong>NOTICIAS DEL STAFF</strong>
+</center>
+<dl style="margin:0 2.5%">
+<?py for post in posts: ?>
+ <dt><strong>#{post['title'] if post['title'] else "Sin asunto"}</strong><br />#{post['id']} : <b class="name">${post['name']}</b> : <span class="date" data-unix="${post['timestamp']}"}>${post['timestamp_formatted']}</span></dt>
+ <dd style="margin-bottom:1em;">#{post['message']}</dd>
+<?py #endfor ?>
+</dl>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/menu.html b/cgi/templates/manage/menu.html
new file mode 100644
index 0000000..d6ffd5e
--- /dev/null
+++ b/cgi/templates/manage/menu.html
@@ -0,0 +1,30 @@
+<style>#adminmenu {text-align:center;}#adminmenu table {display:inline-block;font-size:10pt;margin-top:2px;text-align:left;}
+#adminmenu a {font-weight:bold;}label {vertical-align:top;}dd p {margin:0;}</style>
+<script type="text/javascript" src="/static/js/manage.js"></script>
+<input type="hidden" name="board" value="" />
+<?py if int(rights) < 4: ?>
+<div id="adminmenu">¡Bienvenido, <b><acronym title="Cuenta creada el #{added}">#{username}</acronym></b>! ¡Eres
+<?py if rights == '0': ?><b>Accionista</b>
+<?py elif rights == '1': ?><b>Accionista</b>
+<?py elif rights == '2': ?><span class="developer">Developer</span>
+<?py elif rights == '3': ?><span class="moderator">Moderador</span>
+<?py #endif ?> de #{site_title}!<br />
+<center>
+<table class="reply">
+<tr><td>Principal:</td>
+<td>- <a href="#{cgi_url}manage">Inicio</a> - <a href="#{cgi_url}manage/changepassword">Cambiar contrase&ntilde;a</a> - <a href="#{cgi_url}manage/newschannel">News Channel</a> - <a href="//webmail.bienvenidoainternet.org">Correo</a> - <a href="#{cgi_url}manage/logout">Cerrar sesi&oacute;n</a> -</td></tr>
+<tr><td>Posts:</td>
+<td>- <a href="#{cgi_url}manage/mod">Modbrowse</a> - <a href="#{cgi_url}manage/ipshow">Ver por IP</a> - <a href="#{cgi_url}manage/recyclebin">Papelera de reciclaje</a> - <a href="#{cgi_url}manage/recent_images">Im&aacute;genes recientes</a> -</td></tr>
+<tr><td>Moderaci&oacute;n:</td>
+<td>- <a href="#{cgi_url}manage/reports">Denuncias</a> - <a href="#{cgi_url}manage/ipdelete">Eliminar por IP</a> - <a href="#{cgi_url}manage/bans">Lista de bans</a> - <a href="#{cgi_url}manage/move">Mover hilo</a> - <a href="#{cgi_url}manage/filters">Filtros</a> - <a href="#{cgi_url}manage/quotes">Frases</a> -</td></tr>
+<?py if int(rights) < 3: ?>
+<tr><td>Administraci&oacute;n:</td>
+<td>- <a href="#{cgi_url}manage/rebuild">Reconstruir</a> - <a href="#{cgi_url}manage/news?type=1">Noticias</a> - <a href="#{cgi_url}manage/news?type=2">Twitter</a> - <a href="#{cgi_url}manage/board">Opciones de board</a> - <a href="#{cgi_url}manage/addboard">Agregar board</a> - <a href="#{cgi_url}manage/lockboard">Cerrar board</a> -</td></tr>
+<?py if int(rights) in [0,2]: ?>
+<tr><td>Staff:</td>
+<td>- <a href="#{cgi_url}manage/staff">Miembros</a> - <a href="#{cgi_url}manage/logs">Registro de acciones</a> -</td></tr>
+<?py #endif ?>
+<?py #endif ?>
+</table></center></div>
+<hr />
+<?py #endif ?> \ No newline at end of file
diff --git a/cgi/templates/manage/message.html b/cgi/templates/manage/message.html
new file mode 100644
index 0000000..6c53ecc
--- /dev/null
+++ b/cgi/templates/manage/message.html
@@ -0,0 +1,8 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+ <div class="replymode">#{title if title else "Mensaje"}</div>
+ <p>#{message}</p>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/mod.html b/cgi/templates/manage/mod.html
new file mode 100644
index 0000000..ddc688f
--- /dev/null
+++ b/cgi/templates/manage/mod.html
@@ -0,0 +1,96 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Modbrowse</div>
+<?py if mode == 1: ?>
+<table class="managertable">
+ <tr><th colspan="2">Sección</th><th>Acción</th></tr>
+ <?py for board in boards: ?>
+ <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/mod/#{board['dir']}">Navegar</a>]</td></tr>
+ <?py #endfor ?>
+</table>
+<?py elif mode == 2: ?>
+<table class="managertable">
+<tr>
+ <th>#</th>
+ <th>ID</th>
+ <th style="width:20%;">Asunto</th>
+ <th>Fecha</th>
+ <th style="width:80%;">Mensaje</th>
+ <th>Resp.</th>
+ <th>Acciones</th>
+</tr>
+<?py i = 1 ?>
+<?py for thread in threads: ?>
+<tr>
+ <td>#{i}</td>
+ <td>#{thread['id']}</td>
+ <td><a href="?thread=#{thread['id']}"><b>#{thread['subject']}</b></a></td>
+ <td class="date" data-unix="${thread['timestamp']}">#{thread['timestamp_formatted'][:21]}</td>
+ <td>${thread['message'][:200]}</td>
+ <td>#{thread['length']}</td>
+ <td style="white-space:nowrap;">
+ <a href="#{cgi_url}manage/lock/#{dir}/#{thread['id']}">L#{"-" if thread['locked'] == "1" else "+"}</a>
+ <a href="#{cgi_url}manage/permasage/#{dir}/#{thread['id']}">PS#{"-" if thread['locked'] == "2" else "+"}</a>
+ <a href="#{cgi_url}manage/move/#{dir}/#{thread['id']}">M</a>
+ <a href="#{cgi_url}manage/delete/#{dir}/#{thread['id']}">D</a>
+ <a href="#{cgi_url}manage/delete/#{dir}/#{thread['id']}?ban=true">&</a>
+ <a href="#{cgi_url}manage/ban/#{dir}/#{thread['id']}">B</a>
+ </td>
+</tr>
+<?py i += 1 ?>
+<?py #endfor ?>
+</table>
+<hr />
+[<a href="#{cgi_url}manage/mod" class="return">Volver</a>]
+<?py elif mode == 3: ?>
+<table class="managertable">
+<tr><th colspan="8" style="font-size:16pt;">Hilo: ${posts[0]['subject']} (#{posts[0]['length']})</th></tr>
+<tr><td colspan="8" style="font-size:14pt;text-align:center;"><a href="#{cgi_url}manage/lock/#{dir}/#{posts[0]['id']}">#{"Abrir hilo" if posts[0]['locked'] == "1" else "Cerrar hilo"}</a> /
+<a href="#{cgi_url}manage/permasage/#{dir}/#{posts[0]['id']}">#{"Quitar permasage" if posts[0]['locked'] == "2" else "Permasage"}</a> /
+<a href="#{cgi_url}manage/move/#{dir}/#{posts[0]['id']}">Mover hilo</a></td></tr>
+<tr>
+ <th>#</th>
+ <th>ID</th>
+ <th>Fecha</th>
+ <th>Nombre</th>
+ <th>Mensaje</th>
+ <th>Archivo</th>
+ <th>IP</th>
+ <th>Acción</th>
+</tr>
+<?py i = 1 ?>
+<?py for p in posts: ?>
+<tr>
+ <td>#{i}</td>
+ <td>#{p['id']}</td>
+ <td class="date" data-unix="${p['timestamp']}">${p['timestamp_formatted']}</td>
+ <td><span class="postername">${p['name']}</span></td>
+ <td>${p['message']}</td>
+ <td>
+ <?py if p['file']: ?><a href="/${dir}/src/#{p['file']}" target="_blank"><img src="/${dir}/mobile/${p['thumb']}" /></a><?py #endif ?>
+ </td>
+ <td><a href="#{cgi_url}manage/ipshow?ip=#{p['ip']}">#{p['ip']}</a></td>
+ <td style="white-space:nowrap;">
+ <?py if p['IS_DELETED'] == '0': ?>
+ <a href="#{cgi_url}manage/delete/#{dir}/#{p['id']}">Eliminar</a>
+ <a href="#{cgi_url}manage/delete/#{dir}/#{p['id']}?ban=true">&</a>
+ <a href="/cgi/manage/ban?ip=#{p['ip']}">Ban</a>
+ <?py elif p['IS_DELETED'] == '1': ?>
+ <a href="#{cgi_url}manage/recyclebin/0/restore/#{dir}/#{p['id']}">Recuperar</a>
+ <abbr title="Eliminado por usuario">[1]</abbr>
+ <?py elif p['IS_DELETED'] == '2': ?>
+ <a href="#{cgi_url}manage/recyclebin/0/restore/#{dir}/#{p['id']}">Recuperar</a>
+ <abbr title="Eliminado por staff">[2]</abbr>
+ <?py #endif ?>
+ </td>
+</tr>
+<?py i += 1 ?>
+<?py #endfor ?>
+</table>
+<hr />
+[<a href="#{cgi_url}manage/mod/#{dir}">Volver al panel</a>]
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/move.html b/cgi/templates/manage/move.html
new file mode 100644
index 0000000..8fcc1e9
--- /dev/null
+++ b/cgi/templates/manage/move.html
@@ -0,0 +1,60 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Mover hilo</div>
+<?py if oldboardid and oldthread: ?>
+<form action="#{cgi_url}manage/move/#{oldboardid}/#{oldthread}" method="post">
+<?py else: ?>
+<form action="#{cgi_url}manage/move" method="post">
+<?py #endif ?>
+<table>
+ <tr>
+ <td class="postblock">Board actual</td>
+ <td>
+ <?py if oldboardid and oldthread: ?>
+ <select name="oldboardid" style="width:100%;">
+ <?py for board in boards: ?>
+ <option value="#{board['dir']}"#{' selected="selected"' if oldboardid == board['dir'] else ''}>#{board['dir']} - #{board['name']}</option>
+ <?py #endfor ?>
+ </select>
+ <?py else: ?>
+ <select name="oldboardid" style="width:100%;">
+ <?py for board in boards: ?>
+ <option value="#{board['dir']}">#{board['dir']} - #{board['name']}</option>
+ <?py #endfor ?>
+ </select>
+ <?py #endif ?>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">ID de hilo</td>
+ <td>
+ <?py if oldboardid and oldthread: ?>
+ <input type="text" name="oldthread" style="width:100%;" value="#{oldthread}" />
+ <?py else: ?>
+ <input type="text" name="oldthread" style="width:100%;" />
+ <?py #endif ?>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Mover a</td>
+ <td>
+ <select name="newboardid" style="width:100%;">
+ <?py for board in boards: ?>
+ <option value="#{board['dir']}">#{board['dir']} - #{board['name']}</option>
+ <?py #endfor ?>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td class="postblock">Insertar mensaje</td>
+ <td>
+ <input type="checkbox" name="msg" value="1" />
+ </td>
+ </tr>
+ <tr><td colspan="2"><input type="submit" name="submit" style="width:100%;" value="Mover" /></td></tr>
+</table>
+</form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/quotes.html b/cgi/templates/manage/quotes.html
new file mode 100644
index 0000000..d30a403
--- /dev/null
+++ b/cgi/templates/manage/quotes.html
@@ -0,0 +1,12 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+ <div class="replymode">Quotes</div>
+ <p>Ingresa un mensaje a mostrar por cada linea:</p>
+ <form method="post" action="">
+ <textarea name="data" cols="80" rows="15" style="width:500px;height:250px;">${data}</textarea><br />
+ <input type="submit" name="save" style="width:500px;" value="Guardar" />
+ </form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/rebuild.html b/cgi/templates/manage/rebuild.html
new file mode 100644
index 0000000..3afc057
--- /dev/null
+++ b/cgi/templates/manage/rebuild.html
@@ -0,0 +1,20 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Reconstruir board</div>
+<table class="managertable">
+ <tr><th colspan="2">Sección</th><th colspan="2">Acción</th></tr>
+ <tr><td colspan="2"><b>Home</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!HOME">Reconstruir</a>]</td></tr>
+ <tr><td colspan="2"><b>Noticias</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!NEWS">Reconstruir</a>]</td></tr>
+ <tr><td colspan="2"><b>Ãndices de archivos</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!KAKO">Reconstruir</a>]</td></tr>
+ <tr><td colspan="2"><b>.htaccess</b></td><td colspan="2" style="text-align:center;">[<a href="#{cgi_url}manage/rebuild/!HTACCESS">Reconstruir</a>]</td></tr>
+ <?py for board in boards: ?>
+ <tr><td>/#{board['dir']}/</td><td>#{board['name']}</td><td>[<a href="#{cgi_url}manage/rebuild/#{board['dir']}">Reconstruir frontales</a>]</td><td>[<a href="#{cgi_url}manage/rebuild/#{board['dir']}?everything=1">Reconstruir todo</a>]</td></tr>
+ <?py #endfor ?>
+ <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!ALL" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (frontales)" /></form></td></tr>
+ <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!BBS" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (BBS)" /></form></td></tr>
+ <tr><td colspan="4" align="center"><form action="#{cgi_url}manage/rebuild/!IB" method="get"><input type="submit" style="width:100%" value="Reconstruir todos (IB)" /></form></td></tr>
+</table>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/recent_images.html b/cgi/templates/manage/recent_images.html
new file mode 100644
index 0000000..39f919c
--- /dev/null
+++ b/cgi/templates/manage/recent_images.html
@@ -0,0 +1,24 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<style>.imgs{font-size:0;}.imgs img{vertical-align:top;margin:2px;height:150px;width:auto;}</style>
+<center>
+<div class="replymode">Imágenes recientes</div>
+<form action="#{cgi_url}manage/recent_images" name="recent_images" method="post">
+ <table>
+ <tr><td class="postblock">Número a mostrar</td><td><input type="text" name="images" size="4" /></td></tr>
+ <tr><td colspan="2"><input type="submit" style="width:100%;" value="Enviar" /></td></tr>
+ </table>
+</form>
+<hr />
+<div class="imgs">
+<?py for post in posts: ?>
+ <?py if post['parentid'] != '0': ?>
+ <a href="/#{post['dir']}/res/#{post['parentid']}.html##{post['id']}"><img src="#{boards_url}#{post['dir']}/thumb/#{post['thumb']}" /></a>
+ <?py else: ?>
+ <a href="/#{post['dir']}/res/#{post['id']}.html##{post['id']}"><img src="#{boards_url}#{post['dir']}/thumb/#{post['thumb']}" /></a>
+ <?py #endif ?>
+<?py #endfor ?>
+</div>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/recyclebin.html b/cgi/templates/manage/recyclebin.html
new file mode 100644
index 0000000..b413c9c
--- /dev/null
+++ b/cgi/templates/manage/recyclebin.html
@@ -0,0 +1,72 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Papelera de Reciclaje</div>
+<form name="boardForm" method="get" action="#{cgi_url}manage/recyclebin/0">
+<table>
+<tr>
+ <td class="postblock">Eliminado por</td>
+ <td>
+ <input type="radio" id="type1" name="type" value="1"#{checked(type == 1)} /><label for="type1">Usuario</label>
+ <input type="radio" id="type2" name="type" value="2"#{checked(type == 2)} /><label for="type2">Staff</label>
+ <input type="radio" id="type0" name="type" value="0"#{checked(type == 0)} /><label for="type0">Ambos</label>
+ </td>
+</tr>
+<tr>
+ <td class="postblock">Board</td><td>
+ <select name="board" style="width:100%;">
+ <option value="all">Todos los boards</option>
+<?py for board in boards: ?>
+ <option value="#{board['dir']}"#{selected(board['checked'])}>#{board['dir']} - ${board['name']}</option>
+<?py #endfor ?>
+ </select>
+ </td>
+</tr>
+<tr><td colspan="2"><input type="submit" style="width:100%;" value="Mostrar" /></td></tr>
+</table>
+</form>
+<hr />
+<?py if message: ?>
+${message}
+<hr />
+<?py #endif ?>
+<?py if not skip: ?>
+<form name="deleteForm" method="post" action="#{cgi_url}manage/recyclebin/#{currentpage}">
+ <?py if curboard: ?>
+ <input type="hidden" name="board" value="#{curboard}" />
+ <?py #endif ?>
+ <table class="managertable">
+ <tr>
+ <th></th>
+ <th></th>
+ <th>ID</th>
+ <th>Timestamp</th>
+ <th>Board</th>
+ <th>Tipo</th>
+ <th>IP</th>
+ <th>Mensaje</th>
+ </tr>
+ <?py for post in posts: ?>
+ <tr>
+ <td><a href="#{cgi_url}manage/recyclebin/#{currentpage}/delete/#{post['dir']}/#{post['id']}">X</a><br /><a href="#{cgi_url}manage/recyclebin/#{currentpage}/restore/#{post['dir']}/#{post['id']}">R</a></td>
+ <td><input type="checkbox" name="!i#{post['dir']}/#{post['id']}" id="#{post['dir']}#{post['id']}" value="1" /><label for="#{post['dir']}#{post['id']}"></label></td>
+ <td>#{post['id']}</td>
+ <td class="date" data-unix="${post['timestamp']}">${post['timestamp_formatted']}</td>
+ <td>${post['dir']}</td>
+ <td>${post['IS_DELETED']}</td>
+ <td>${post['ip']}</td>
+ <td>#{post['message']}</td>
+ </tr>
+ <?py #endfor ?>
+ <tr><td colspan="8" align="center"><input name="deleteall" type="submit" value="Eliminar seleccionados" /></td></tr>
+ </table>
+</form>
+<hr />
+<div style="font-size:larger">#{navigator}</div>
+<?py else: ?>
+ No hay posts.
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/reports.html b/cgi/templates/manage/reports.html
new file mode 100644
index 0000000..f47ec38
--- /dev/null
+++ b/cgi/templates/manage/reports.html
@@ -0,0 +1,58 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Reportes</div>
+<?py if message: ?>
+${message}
+<?py #endif ?>
+<form name="boardForm" method="get" action="#{cgi_url}manage/reports/0">
+<table>
+ <tr>
+ <td class="postblock">Board</td>
+ <td>
+ <select name="board">
+ <option value="all">Todos los boards</option>
+<?py for board in boards: ?>
+ <option value="#{board['dir']}"#{selected(board['checked'])}>#{board['dir']} - #{board['name']}</option>
+<?py #endfor ?>
+ </select>
+ <td><input type="submit" value="Mostrar" /></td>
+ </td></tr>
+</table>
+</form>
+
+<form name="ignoreForm" method="post" action="#{cgi_url}manage/reports/#{currentpage}">
+<?py if curboard: ?>
+<input type="hidden" name="board" value="#{board}" />
+<?py #endif ?>
+<hr />
+<table class="managertable">
+<tr>
+ <th></th>
+ <th></th>
+ <th>Fecha</th>
+ <th>Post</th>
+ <th>IP Post</th>
+ <th>Raz&oacute;n</th>
+ <th>IP Denuncia</th>
+</tr>
+<?py for report in reports: ?>
+<tr>
+ <td> <a href="#{cgi_url}manage/reports/#{currentpage}/ignore/#{report['id']}">X</a> </td>
+ <td><input type="checkbox" name="i#{report['id']}" id="i#{report['id']}" value="1" /><label for="i#{report['id']}"></label></td>
+ <td class="date" data-unix="${report['timestamp']}">${report['timestamp_formatted']}</td>
+ <td><a href="#{report['link']}">${report['link']}</a></td>
+ <td><a href="#{cgi_url}manage/ipshow?ip=${report['ip']}">${report['ip']}</a></td>
+ <td>${report['reason']}</td>
+ <td><a href="#{cgi_url}manage/ipshow?ip=${report['reporterip']}">${report['reporterip']}</a></td>
+</tr>
+<?py #endfor ?>
+<tr>
+ <td colspan="8" style="text-align:center;"><input name="ignore" type="submit" value="Ignorar seleccionados" /></td>
+</tr>
+</table>
+</form>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/manage/search.html b/cgi/templates/manage/search.html
new file mode 100644
index 0000000..6c2ec6f
--- /dev/null
+++ b/cgi/templates/manage/search.html
@@ -0,0 +1,27 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<center>
+<div class="replymode">Registro de búsqueda</div>
+<table class="managertable">
+ <tr>
+ <th>ID</th>
+ <th>Fecha</th>
+ <th>Búsqueda</th>
+ <th>En</th>
+ <th>Resultados</th>
+ <th>Por</th>
+ </tr>
+<?py for log in search: ?>
+ <tr>
+ <td>${log['id']}</td>
+ <td class="date" data-unix="${log['timestamp']}">${log['timestamp_formatted']}</td>
+ <td>${log['keyword']}</td>
+ <td>${"[A] " if log['archive'] else ""}${"Global" if log["ita"] == "" else log["ita"]}</td>
+ <td>${log['res']}</td>
+ <td><a href="#{cgi_url}manage/ipshow?ip=${log['ip']}">${log['ip']}</a></td>
+ </tr>
+<?py #endfor ?>
+</table>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/manage/staff.html b/cgi/templates/manage/staff.html
new file mode 100644
index 0000000..787a843
--- /dev/null
+++ b/cgi/templates/manage/staff.html
@@ -0,0 +1,63 @@
+<?py include('templates/base_top.html') ?>
+<?py include('templates/manage/menu.html') ?>
+<?py from tenjin.helpers.html import * ?>
+<center>
+<div class="replymode">Staff</div>
+<?py if mode == 0: ?>
+ <table class="managertable">
+ <tr>
+ <th>ID</th>
+ <th>Nombre</th>
+ <th>Nivel</th>
+ <th>Última actividad</th>
+ <th>Acciones</th>
+ </tr>
+ <?py for member in staff: ?>
+ <tr>
+ <td>${member['id']}</td>
+ <td><b>${member['username']}</b></td>
+ <td>${member['rights']}</td>
+ <td class="date" data-unix="${member['lastactivestamp']}">${member['lastactive']}</td>
+ <td>
+ [<a href="#{cgi_url}manage/staff/edit/#{member['id']}">Editar</a>]
+ [<a href="#{cgi_url}manage/staff/delete/#{member['id']}">Eliminar</a>]
+ </td>
+ </tr>
+ <?py #endfor ?>
+ <tr>
+ <td colspan="5"><form action="#{cgi_url}manage/staff/add" method="get"><input type="submit" style="width:100%;" value="Agregar miembro" /></form></td>
+ </tr>
+ </table>
+<?py elif mode == 1: ?>
+<form action="#{cgi_url}manage/staff/#{action}" method="post">
+<table>
+ <tr>
+ <td class="postblock">Nombre</td>
+ <td><input type="text" name="username" value="${member_username}" style="width:100%;" /></td>
+ </tr>
+ <?py if not member: ?>
+ <tr>
+ <td class="postblock">Contraseña</td>
+ <td><input type="password" name="password" style="width:100%;"/></td>
+ </tr>
+ <?py #endif ?>
+ <tr>
+ <td class="postblock">Nivel</td>
+ <td>
+ <select name="rights" style="width:100%;">
+ <option value="3"#{selected(member_rights == '3')}>Moderador</option>
+ <option value="2"#{selected(member_rights == '2')}>Developer</option>
+ <option value="1"#{selected(member_rights == '1')}>Administrador</option>
+ <option value="0"#{selected(member_rights == '0')}>Super-Administrador</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2"><input type="submit" name="submit" style="width:100%;" value="${submit}"/></td>
+ </tr>
+</table>
+</form>
+<?py #endif ?>
+</center>
+<hr />
+<?py include('templates/base_bottom.html') ?> \ No newline at end of file
diff --git a/cgi/templates/mobile/base_top.html b/cgi/templates/mobile/base_top.html
new file mode 100644
index 0000000..6a6c5bd
--- /dev/null
+++ b/cgi/templates/mobile/base_top.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<?py if (replythread and threads) or board: ?>
+ <title>#{board_name}@Bienvenido a Internet Móvil</title>
+<?py else: ?>
+ <title>Bienvenido a Internet Móvil</title>
+<?py #endif ?>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="shortcut icon" href="#{static_url}img/favicon.ico" />
+ <link rel="stylesheet" type="text/css" href="#{static_url}css/mobile.css?v=8" />
+ <script type="text/javascript" src="#{static_url}js/mobile.js?v=9"></script>
+</head>
diff --git a/cgi/templates/mobile/board.html b/cgi/templates/mobile/board.html
new file mode 100644
index 0000000..70b8461
--- /dev/null
+++ b/cgi/templates/mobile/board.html
@@ -0,0 +1,55 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="img"><a name="top"></a>
+<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#form">&#9660;</a></div></div>
+<?py for thread in threads: ?>
+<div id="thread">
+<?py for post in thread['posts']: ?>
+ <?py if post['IS_DELETED'] == "1": ?>
+ <div class="pst"><h3 class="del"><a name="#{post['id']}"></a>No.#{post['id']} eliminado por el usuario.</h3></div>
+ <?py elif post['IS_DELETED'] == "2": ?>
+ <div class="pst"><h3 class="del"><a name="#{post['id']}"></a>No.#{post['id']} eliminado por miembro del staff.</h3></div>
+ <?py else: ?>
+ <?py if post['parentid'] == "0": ?>
+ <div class="first"><h1>#{post["subject"]} <span>(#{thread['length']})</span></h1>
+ <?py else: ?>
+ <div class="pst">
+ <?py if post['subject']: ?>
+ <h2>#{post["subject"]}</h2>
+ <?py #endif ?>
+ <?py #endif ?><h3><a href="#" class="num" name="#{post['id']}">#{post['id']}</a>#{post['name']} #{post['tripcode']} #{post['timestamp_formatted']}</h3>
+ <?py if post['file']: ?><a href="/#{board}/src/#{post['file']}" target="_blank" class="thm"><img src="/#{board}/mobile/#{post['thumb']}" /><br />#{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}</a><?py #endif ?>
+ <div class="msg">#{post['message']}</div></div>
+ <?py #endif ?>
+<?py #endfor ?>
+<?py if threads[0]['posts'][0]['locked'] != "1": ?>
+<a href="./#{thread['id']}" id="n">Recargar</a><span id="n2"></span>
+<?py #endif ?>
+<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#top">&#9650;</a></div></div>
+<?py if threads[0]['posts'][0]['locked'] == "1": ?>
+ <div class="warn red" style="text-align:center;">El hilo ha sido cerrado. Ya no se puede postear en &eacute;l.</div>
+<?py else: ?>
+<form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{replythread}" /><input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" />
+ <div style="display:none;"><input type="text" name="name" /><input type="text" name="email" /></div>
+ <?py if not disable_subject: ?>
+ <input class="fld" type="text" name="subject" placeholder="Asunto (opcional)" />
+ <?py #endif ?>
+ <?py if not disable_name: ?>
+ <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" />
+ <?py #endif ?>
+ <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" />
+ <textarea name="message" rows="6"></textarea>
+<?py if allow_image_replies: ?>
+ <div class="file"><input type="file" name="file" class="fld" />
+ <?py if allow_spoilers: ?>
+ <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label>
+ <?py #endif ?></div>
+<?py #endif ?>
+ <input id="post" type="submit" value="Responder" />
+</form>
+<?py #endif ?>
+</div>
+<?py #endfor ?>
+<a name="form"></a>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/error.html b/cgi/templates/mobile/error.html
new file mode 100644
index 0000000..00ae4f4
--- /dev/null
+++ b/cgi/templates/mobile/error.html
@@ -0,0 +1,6 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="img">
+<div class="top"><a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a>Error</div><br />
+<hr size="1"><br /><br /><div style="color:red;font-size:x-large;font-weight:bold;text-align:center;">#{error}</div><br /><br /><hr size="1">
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/latest.html b/cgi/templates/mobile/latest.html
new file mode 100644
index 0000000..615b21c
--- /dev/null
+++ b/cgi/templates/mobile/latest.html
@@ -0,0 +1,14 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="txt">
+<div class="top">
+ <a href="/movil.html"><img src="#{static_url}css/img/0info.png" /><br />Info</a>
+ Bienvenido a Internet Móvil
+</div>
+<div class="bar"><a href="//m.bienvenidoainternet.org">Secciones</a><a href="/cgi/mobilehome" class="sel">Hilos activos</a><a href="/cgi/mobilenewest">Nuevos hilos</a></div>
+<div class="list">
+ <?py for thread in latest_age: ?>
+ <a href="/cgi/mobileread${thread['url']}">#{thread['content']}<div>${thread['board_fulln']} <span>R:<span>#{int(thread['length'])-1}</span></span></div></a>
+ <?py #endfor ?>
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/newest.html b/cgi/templates/mobile/newest.html
new file mode 100644
index 0000000..37fd67f
--- /dev/null
+++ b/cgi/templates/mobile/newest.html
@@ -0,0 +1,14 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="txt">
+<div class="top">
+ <a href="/movil.html"><img src="#{static_url}css/img/0info.png" /><br />Info</a>
+ Bienvenido a Internet Móvil
+</div>
+<div class="bar"><a href="//m.bienvenidoainternet.org">Secciones</a><a href="/cgi/mobilehome">Hilos activos</a><a href="/cgi/mobilenewest" class="sel">Nuevos hilos</a></div>
+<div class="list">
+ <?py for thread in newthreads: ?>
+ <a href="/cgi/mobileread${thread['url']}">#{thread['content']}<div>${thread['board_fulln']}</div></a>
+ <?py #endfor ?>
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/threadlist.html b/cgi/templates/mobile/threadlist.html
new file mode 100644
index 0000000..edb81eb
--- /dev/null
+++ b/cgi/templates/mobile/threadlist.html
@@ -0,0 +1,43 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="img">
+<div class="top">
+ <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a>
+ #{board_name}
+</div>
+<?py if mode == 1: ?>
+ <div class="bar"><a href="#{cgi_url}mobile/#{board}" class="sel">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Cat&aacute;logo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div>
+ <?py for thread in more_threads: ?>
+ <div class="prev">
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><img class="thm" src="/#{board}/mobile/#{thread['thumb']}" />
+ <b>#{thread["subject"]}</b> (R:#{int(thread["length"])-1})</a>
+ <h3>#{thread['name']} #{thread['tripcode']} #{thread['timestamp_formatted']}</h3>
+ #{thread['message']}#{" [...]" if thread['shortened'] else ""}
+ <?py if thread['lastreply']: ?>
+ <div class="pst"><h3>#{thread['lastreply']['name']} #{thread['lastreply']['tripcode']} #{thread['lastreply']['timestamp_formatted']}</h3>
+ #{thread['lastreply']['message']}#{" [...]" if thread['lastreply']['shortened'] else ""}</div>
+ <?py #endif ?>
+ </div>
+ <?py #endfor ?>
+<?py elif mode == 2: ?>
+ <div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}" class="sel">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Cat&aacute;logo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div>
+ <div class="search"><input id="search" placeholder="Buscar en asuntos" style="padding:7px;" type="text"></div>
+ <div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div>
+ <div id="to_sort" class="list">
+ <?py i = 1 ?>
+ <?py for thread in more_threads: ?>
+ <a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><strong>#{thread["subject"]}</strong>: #{thread['message']}#{" [...]" if thread['shortened'] else ""}<br />
+ <span class="info"><span>&Uacute;ltima: #{thread['lastreply']['timestamp_formatted'] if thread['lastreply'] else thread['timestamp_formatted']}</span> Respuestas: <b>#{int(thread["length"])-1}</b></span></a>
+ <?py i += 1 ?>
+ <?py #endfor ?>
+ </div>
+<?py else: ?>
+ <div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}" class="sel">Cat&aacute;logo</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div>
+ <div class="search"><input id="catsearch" placeholder="Buscar en catálogo" style="padding:7px;" type="text"></div>
+ <div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div>
+ <div id="to_sort" style="text-align:center;margin-top:0.5em;">
+ <?py i = 1 ?>
+ <?py for thread in more_threads: ?><a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" class="cat" href="#{cgi_url}mobileread/#{board}/#{thread['id']}"><img src="/#{board}/mobile/#{thread['thumb']}" /><br />(#{int(thread["length"])-1}R) <strong>#{thread["subject"]}</strong>: #{thread['message']}#{" [...]" if thread['shortened'] else ""}</a><?py i += 1 ?><?py #endfor ?>
+ </div>
+<?py #endif ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/txt_newthread.html b/cgi/templates/mobile/txt_newthread.html
new file mode 100644
index 0000000..b19d2fa
--- /dev/null
+++ b/cgi/templates/mobile/txt_newthread.html
@@ -0,0 +1,35 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="#{"txt" if board_type == '1' else "img"}">
+<div class="top">
+ <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a>
+ #{board_name}
+</div>
+<?py if board_type == '1': ?>
+<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}" class="sel">Nuevo hilo</a></div>
+<?py else: ?>
+<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Lista</a><a href="#{cgi_url}mobilecat/#{board}">Cat&aacute;logo</a><a href="#{cgi_url}mobilenew/#{board}" class="sel">Nuevo hilo</a></div>
+<?py #endif ?>
+<form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /> <input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" />
+ <div style="display:none;"><input type="text" name="name" maxlength="50" /><input type="text" name="email" maxlength="50" /></div>
+ <?py if not disable_subject: ?>
+ <input class="fld imp" type="text" name="subject" placeholder="Asunto#{" (opcional)" if board_type == '0' else ""}" maxlength="100" />
+ <?py #endif ?>
+ <?py if not disable_name: ?>
+ <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" maxlength="50" />
+ <?py #endif ?>
+ <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" maxlength="50" />
+ <textarea name="message" rows="#{"8" if board_type == '1' else "6"}"></textarea>
+<?py if allow_images: ?>
+ <div class="file"><input type="file" name="file" class="fld" />
+ <?py if allow_spoilers: ?>
+ <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label>
+ <?py #endif ?></div>
+<?py #endif ?>
+ <input id="post" type="submit" value="Crear nuevo hilo" />
+</form>
+<?py if allow_images: ?>
+ <div class="rules">Formatos permitidos: #{', '.join(supported_filetypes).upper()}<br />Tamaño máximo: #{maxsize} KB</div>
+<?py #endif ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/txt_thread.html b/cgi/templates/mobile/txt_thread.html
new file mode 100644
index 0000000..8a19a94
--- /dev/null
+++ b/cgi/templates/mobile/txt_thread.html
@@ -0,0 +1,74 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="txt">
+<a name="top"></a>
+<?py for thread in threads: ?>
+<div class="nav"><div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#form">&#9660;</a></div></div>
+<div id="nav2">
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}">Ver hilo completo</a>
+<?py if thread['length'] > 51: ?>
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l25" rel="nofollow">Últimos 25</a>
+<?py #endif ?>
+<?py if thread['length'] > 50: ?>
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/-50" rel="nofollow">Primeros 50</a>
+<?py #endif ?>
+<?py r = range(thread['length'] / 50) ?>
+<?py for i in r[:-1]: ?>
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{(i+1)*50+1}-#{(i+2)*50}" rel="nofollow">#{(i+1)*50+1}-#{(i+2)*50}</a>
+<?py #endfor ?>
+<?py if r: ?>
+ <a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{(r[-1]+1)*50+1}-#{(r[-1]+2)*50}" rel="nofollow">#{(r[-1]+1)*50+1}-</a>
+<?py #endif ?>
+</div>
+<?py if thread['length'] > 1000: ?>
+ <div class="stop red">■ El hilo superó los 1000 mensajes y ha sido cerrado.</div>
+<?py elif thread['length'] > 950: ?>
+ <div class="warn red">■ El hilo ha recibido más de 950 mensajes. Límite: 1000</div>
+<?py elif thread['length'] > 900: ?>
+ <div class="warn yellow">■ El hilo ha recibido más de 900 mensajes. Límite: 1000</div>
+<?py #endif ?>
+<div id="thread">
+<h1>#{thread['subject']} <span>(#{thread['length']})</span></h1>
+<?py for post in thread['posts']: ?>
+<?py if post['IS_DELETED'] == '1': ?>
+<div class="pst"><h3 class="del"><a href="#" class="num">#{str(post['num']).zfill(4)}</a> Eliminado por el usuario.</h3></div>
+<?py elif post['IS_DELETED'] == '2': ?>
+<div class="pst"><h3 class="del"><a href="#" class="num">#{str(post['num']).zfill(4)}</a> Eliminado por miembro del staff.</h3></div>
+<?py else: ?>
+<div id="p#{post['id']}" class="pst">
+ <h3><a href="#" class="num">#{str(post['num']).zfill(4)}</a> #{post['name']} #{post['tripcode']}</h3>
+ <?py if post['file']: ?><a href="/#{board}/src/#{post['file']}" target="_blank" class="thm"><img src="/#{board}/mobile/#{post['thumb']}" /><br />#{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}</a><?py #endif ?>
+ <div class="msg">#{post['message']}</div>
+ <h4>#{post['timestamp_formatted']}</h4>
+</div>
+<?py #endif ?>
+<?py #endfor ?>
+<?py if thread['locked'] != '1': ?>
+<a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/#{thread['length']}-n" id="n">Ver nuevos posts</a><span id="n2"></span>
+<?py #endif ?>
+<div class="nav">
+ <div><a href="//m.bienvenidoainternet.org">Home</a><a href="#{cgi_url}mobile/#{board}/">Volver</a><a href="#top">&#9650;</a></div>
+ <?py if nextrange: ?>
+ <div><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}">Hilo completo</a><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/-50">Primeros 50</a><a href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">Últimos 25</a></div>
+ <?py #endif ?>
+</div>
+<?py if thread['locked'] != '1': ?>
+ <form name="postform" id="postform" action="/cgi/post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="mobile" value="true" /><input type="hidden" name="password" value="" />
+ <div style="display:none"><input type="text" name="name" /><input type="text" name="email" /></div>
+ <input class="fld" type="text" name="fielda" placeholder="Nombre (opcional)" />
+ <input class="fld" type="text" name="fieldb" placeholder="E-mail (opcional)" />
+ <textarea name="message" rows="6"></textarea>
+<?py if allow_image_replies: ?>
+ <div class="file"><input type="file" name="file" class="fld" />
+ <?py if allow_spoilers: ?>
+ <label class="fld"><input type="checkbox" name="spoil" /> Spoiler</label>
+ <?py #endif ?></div>
+<?py #endif ?>
+ <input id="post" type="submit" value="Responder" />
+ </form>
+<?py #endif ?>
+</div>
+<a name="form"></a>
+<?py #endfor ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mobile/txt_threadlist.html b/cgi/templates/mobile/txt_threadlist.html
new file mode 100644
index 0000000..5e3d133
--- /dev/null
+++ b/cgi/templates/mobile/txt_threadlist.html
@@ -0,0 +1,26 @@
+<?py include('templates/mobile/base_top.html') ?>
+<body class="txt">
+<div class="top">
+ <a href="//m.bienvenidoainternet.org"><img src="#{static_url}css/img/0back.png" /><br />Home</a>
+ #{board_name}
+</div>
+<?py if mode == 1: ?>
+<div class="bar"><a href="#{cgi_url}mobile/#{board}" class="sel">Portada</a><a href="#{cgi_url}mobilelist/#{board}">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div>
+<?py else: ?>
+<div class="bar"><a href="#{cgi_url}mobile/#{board}">Portada</a><a href="#{cgi_url}mobilelist/#{board}" class="sel">Todos los hilos</a><a href="#{cgi_url}mobilenew/#{board}">Nuevo hilo</a></div>
+<div class="search"><input id="search" placeholder="Buscar en asuntos" type="text"></div>
+<div class="ord"><span>Orden:</span><a data-sort="0" class="sel" href="#">Normal</a><a data-sort="1" href="#">Nuevo</a><a data-sort="2" href="#">Viejo</a><a data-sort="3" href="#">Más</a><a data-sort="4" href="#">Menos</a></div>
+<?py #endif ?>
+<div id="to_sort" class="list">
+ <?py i = 1 ?>
+ <?py for thread in more_threads: ?>
+ <?py if int(thread["length"]) > 10: ?>
+ <a data-num="${i}" data-res="${thread['length']}" data-id="${thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">#{thread['subject']}<br /><span class="info"><span>Última: #{timestamps[i-1][1]}</span> Respuestas: <b>#{thread['length']}</b></span></a>
+ <?py else: ?>
+ <a data-num="${i}" data-res="#{thread['length']}" data-id="#{thread['id']}" href="#{cgi_url}mobileread/#{board}/#{thread['timestamp']}/l10">#{thread['subject']}<br /><span class="info"><span>Última: #{timestamps[i-1][1]}</span> Respuestas: <b>#{thread['length']}</b></span></a>
+ <?py #endif ?>
+ <?py i += 1 ?>
+ <?py #endfor ?>
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/mod.html b/cgi/templates/mod.html
new file mode 100644
index 0000000..21a35f6
--- /dev/null
+++ b/cgi/templates/mod.html
@@ -0,0 +1,86 @@
+<!-- MOD/S3M/XM module player for Web Audio (c) 2012-2015 Firehawk/TDA (firehawk@haxor.fi) -->
+<!-- Modificado para funcionar con Bienvenido a Internet BBS/IB -->
+<html>
+ <head>
+ <title>MOD/S3M/XM module player for Web Audio</title>
+ <meta name="description" content="A MOD/S3M/XM module player in Javascript using the Web Audio API.">
+ <link rel="stylesheet" href="/firehawk/style.css" type="text/css" media="screen" />
+ <script type="text/javascript" src="/firehawk/jquery-2.1.1.js"></script>
+ <script type="text/javascript" src="/firehawk/utils.js"></script>
+ <script type="text/javascript" src="/firehawk/player.js"></script>
+ <script type="text/javascript" src="/firehawk/pt.js"></script>
+ <script type="text/javascript" src="/firehawk/st3.js"></script>
+ <script type="text/javascript" src="/firehawk/ft2.js"></script>
+ <script type="text/javascript" src="/firehawk/ui.js"></script>
+ </head>
+ <body data-module="/#{board}/src/#{modfile}">
+ <div id="outercontainer">
+ <div id="headercontainer">
+ <div style="margin-left:8px;float:left">MOD/S3M/XM module player for Web Audio</div>
+ <div style="margin-right:8px;float:right">(c) 2012-2015 Firehawk/<a class="tdalink" href="http://tda.haxor.fi/" target="_blank">TDA</a></div>
+ <div style="clear:both;"></div>
+ </div>
+ <div id="innercontainer">
+ <div id="modsamples">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+</div>
+ <div style="position:relative;top:8px;margin-bottom:8px;">
+ <span id="modtitle">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
+ <span id="modinfo">('&nbsp;&nbsp;&nbsp;&nbsp;')</span>
+ <span id="modtimer"></span>
+ <br/><br/>
+ <a href="#" id="go_back">[&lt;&lt;]</a>
+ <a href="#" id="play">[reproducir]</a>
+ <a href="#" id="pause">[pausa]</a>
+ <a href="#" id="go_fwd">[&gt;&gt;]</a>
+ <span style="white-space:pre;"> </span>
+ <a href="#" title="Repeat song" id="modrepeat">[rept]</a>
+ <a class="down" title="Stereo separation" href="#" id="modpaula">[)oo(]</a>
+ <a class="down" title="Visualization type" href="#" id="modvis">[trks]</a>
+ <a title="Amiga A500 lowpass filter" href="#" id="modamiga">[filt]</a>
+ </div>
+ <div id="modchannels"><div id="even-channels"></div><div id="odd-channels"></div></div>
+ <div id="modpattern"></div>
+ <div style="clear:both"></div>
+ <div id="infotext">
+ Esta es una instancia local del reproductor de MODs por Firehawk - <a href="https://twitter.com/janihalme" style="color:#cce;">Twitter</a> / <a href="mailto:firehawk@haxor.fi" style="color:#cce">firehawk@haxor.fi</a>.<br/>Código fuente disponible en <a style="color:#cce;" target="_blank" href="https://github.com/jhalme/webaudio-mod-player">GitHub</a> bajo licencia MIT.
+ <!--
+ The player has been tested on Chrome 14+, Firefox 24+, Safari 6+ and Edge 20+ so far. <span style="color:#faa">Disable AdBlock if you get cuts or stuttering!</span>
+ To report bugs, suggest features or request songs, contact me on <a href="https://twitter.com/janihalme" style="color:#cce;">Twitter</a> or
+ email <a href="mailto:firehawk@haxor.fi" style="color:#cce">firehawk@haxor.fi</a>.
+ Source code available on .-->
+ </div>
+ </div>
+
+
+ </div>
+ </body>
+</html>
diff --git a/cgi/templates/navbar.html b/cgi/templates/navbar.html
new file mode 100644
index 0000000..1655f0b
--- /dev/null
+++ b/cgi/templates/navbar.html
@@ -0,0 +1,16 @@
+<a id="noticias" href="/noticias/">Actualidad</a>
+<a id="tech" href="/tech/">Tecnolog&iacute;a</a>
+<a id="juegos" href="/juegos/">Juegos</a>
+<a id="musica" href="/musica/">M&uacute;sica</a>
+<a id="tv" href="/tv/">TV y Cine</a>
+<a id="letras" href="/letras/">Humanidades</a>
+<a id="zonavip" href="/zonavip/">Club VIP</a>
+<a id="world" href="/world/">World Lobby</a>
+|
+<a id="img" href="/img/">Imágenes</a>
+<a id="2d" href="/2d/">二次元画åƒ</a>
+<a id="n" href="/n/">Naturaleza</a>
+<a id="o" href="/o/">Oekaki</a>
+<a id="0" href="/0/">Cero</a>
+|
+<a id="bai" href="/bai/">Meta</a> \ No newline at end of file
diff --git a/cgi/templates/paint.html b/cgi/templates/paint.html
new file mode 100644
index 0000000..476babe
--- /dev/null
+++ b/cgi/templates/paint.html
@@ -0,0 +1,79 @@
+<?py include('templates/base_top.html') ?>
+<?py if selfy: ?>
+<script type="text/javascript" src="#{static_url}js/palette_selfy.js"></script>
+<?py #endif ?>
+<center>
+<?py if applet == 'shipainter': ?>
+<applet id="oekaki" code="c.ShiPainter.class" archive="#{boards_url}oek_temp/spainter_all.jar" width="#{width+250}" height="#{height+280}" mayscript="">
+ <?py for key, value in params.iteritems(): ?>
+ <param name="#{key}" value="#{value}" />
+ <?py #endfor ?>
+</applet>
+<?py if selfy: ?>
+<script type="text/javascript">palette_selfy();</script>
+<?py #endif ?>
+<?py elif applet == 'neo': ?>
+<link rel="stylesheet" href="#{static_url}js/paintbbs/PaintBBS-1.3.4.css" type="text/css" />
+<script src="#{static_url}js/paintbbs/PaintBBS-1.3.4.js" charset="UTF-8"></script>
+<applet-dummy id="oekaki" name="paintbbs" width="#{width+250}" height="#{height+280}">
+<param name="image_width" value="#{width}">
+<param name="image_height" value="#{height}">
+<param name="image_bkcolor" value="#FFFFFF">
+<param name="image_size" value="0">
+<param name="undo" value="90">
+<param name="undo_in_mg" value="15">
+<param name="color_text" value="#EFEFFF">
+<param name="color_bk" value="#E8EFFF">
+<param name="color_bk2" value="#D5D8EF">
+<param name="color_icon" value="#A1B8D8">
+<param name="color_iconselect" value="#000000">
+<param name="url_save" value="/oek_temp/save.php?applet=paintbbs">
+<param name="url_exit" value="#{cgi_url}oekaki/finish/#{board}/#{replythread}">
+<param name="poo" value="false">
+<param name="send_advance" value="true">
+<param name="thumbnail_width" value="100%">
+<param name="thumbnail_height" value="100%">
+<param name="tool_advance" value="true">
+<param name="tool_color_button" value="#D2D8FF">
+<param name="tool_color_button2" value="#D2D8FF">
+<param name="tool_color_text" value="#5A5781">
+<param name="tool_color_bar" value="#D2D8F0">
+<param name="tool_color_frame" value="#7474AB">
+<?py if edit: ?>
+<param name="image_canvas" value="#{edit}">
+<?py #endif ?>
+</applet-dummy>
+<?py elif applet == 'wpaint': ?>
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.1.10.2.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.core.1.10.3.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.widget.1.10.3.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js"></script>
+<link rel="Stylesheet" type="text/css" href="#{static_url}js/wpaint/lib/wColorPicker.min.css" />
+<script type="text/javascript" src="#{static_url}js/wpaint/lib/wColorPicker.min.js"></script>
+<link rel="Stylesheet" type="text/css" href="#{static_url}js/wpaint/wPaint.min.css" />
+<script type="text/javascript" src="#{static_url}js/wpaint/wPaint.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/plugins/main/wPaint.menu.main.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/plugins/text/wPaint.menu.text.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js"></script>
+<script type="text/javascript" src="#{static_url}js/wpaint/plugins/file/wPaint.menu.main.file.min.js"></script>
+<div id="wPaint" style="position:relative; width:#{width}px; height:#{height}px; background-color:#7a7a7a; margin:70px auto 20px auto;"></div>
+<script type="text/javascript" src="#{static_url}js/wpaint/bai.js"></script>
+<?py elif applet == 'tegaki': ?>
+<form id="imgform" data-w="#{width}" data-h="#{height}" action="#{cgi_url}oekaki/finish/#{board}/#{replythread}" method="post">
+<input type="hidden" name="filebase" id="filebase" />
+</form>
+<link rel="Stylesheet" type="text/css" href="#{static_url}js/tegaki/tegaki.css" />
+<script type="text/javascript" src="#{static_url}js/tegaki/tegaki.js"></script>
+<div id="buttons"><button id="topen">Abrir Tegaki</button></div>
+<div style="font-size:20pt" id="status"></div>
+<?py #endif ?>
+
+<br /><br /><br />
+<div id="links">
+<a href="#{boards_url}#{board}">Volver</a><br />
+<a id="finish" href="#{cgi_url}oekaki/finish/#{board}/#{replythread}">Recuperar dibujo guardado</a>
+</div>
+</center>
+<br />
+<?py include('templates/base_bottom.html') ?>
diff --git a/cgi/templates/redirect.html b/cgi/templates/redirect.html
new file mode 100644
index 0000000..172425d
--- /dev/null
+++ b/cgi/templates/redirect.html
@@ -0,0 +1,12 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Has posteado en Bienvenido a Internet BBS/IB</title>
+<meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8" />
+<meta http-equiv="refresh" content="0;url=#{url}" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body style="text-align:center;">
+<h1>Gracias por tu post</h1><h3>${message}</h3><em>(por favor espera)</em>
+<?py if timetaken: ?><p style="font-size:small">Tiempo usado: #{timetaken}</p><?py #endif ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/report.html b/cgi/templates/report.html
new file mode 100644
index 0000000..d37ca6d
--- /dev/null
+++ b/cgi/templates/report.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<?py if finished: ?><title>Post denunciado</title>
+<?py else: ?><title>Denunciar post ${postshow}</title>
+<?py #endif ?>
+<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<style>*{box-sizing:border-box} body{max-width:350px;margin:0 auto;text-align:justify} h1{text-align:left} a{color:#06C} a:active{color:#F30}
+input{border:1px solid #BBB;width:100%} .long{display:block}</style>
+</head>
+<body>
+<?py if finished: ?>
+<hr /><h1>Post denunciado.</h1>
+<hr /><a href="javascript:void(0)" onclick="history.go(-2);return(false);" class="long">Volver</a><hr />
+<?py else: ?>
+<hr /><h1>Formulario de denuncia</h1>
+<hr />Para pedir que el post <b>${postshow}</b> sea eliminado, indica una razón y presiona el botón [Enviar denuncia].
+<hr /><a href="javascript:void(0)" onclick="history.go(-1);return(false);" class="long">Volver</a>
+<hr /><form method="post" action=""><input type="text" name="reason" placeholder="Razón" maxlength="100" style="margin-bottom:0.5em;" /><input type="submit" value="Enviar denuncia" /></form>
+<hr />Este formulario no es para eliminar tu propio post.
+<?py if txt: ?>Para eliminar tu propio post debes presionar el botón <u>del</u> que aparece a la derecha de tu post cuando le pones el cursor encima. [<a href="/faq.html#del">info</a>]
+<?py else: ?>Para eliminar tu propio post debes chequear la caja que se encuentra en la parte superior izquierda de tu post y luego presionar el botón "Eliminar" que se encuentra al final de la página. [<a href="/faq.html#del">info</a>]<?py #endif ?>
+<hr />Normalmente eliminamos los mensajes que son considerados spam o flood. Si deseas pedir la prohibición de acceso a algún usuario persistente, te recomendamos hacerlo en la sección <a href="/bai/">meta</a>.
+<hr /><a href="mailto:burocracia@bienvenidoainternet.org" class="long">Contacto</a><hr />
+<?py #endif ?>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/revision.html b/cgi/templates/revision.html
new file mode 100644
index 0000000..1e9b46b
--- /dev/null
+++ b/cgi/templates/revision.html
@@ -0,0 +1 @@
+0.8.7
diff --git a/cgi/templates/stats.html b/cgi/templates/stats.html
new file mode 100644
index 0000000..dd0e5ab
--- /dev/null
+++ b/cgi/templates/stats.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Estadísticas@Bienvenido a Internet</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<style>
+body{font-family:arial,sans-serif;background:#090909;color:#fdfdfd;margin:0;text-align:center}
+a,a:visited{color:#fdfdfd;text-decoration:none}
+a:hover{text-decoration:underline}
+hr{margin:1em 0}
+span{display:inline-block}
+#title{margin-top:1em}
+#title a{text-decoration:none}
+.t1{font-size:1.8em;display:inline-block;line-height:1em}
+.t2{font-size:1em;margin-top:2px}
+h2{font-size:1.5em;margin:0 0 .2em 0}
+table{font-size:1.4em;margin:0 auto 1em}
+th,td{padding:10px;border-top:1px solid #222;border-left:1px solid #222}
+.boards{padding:0}
+td a{display:block;padding:10px}
+.desc tr{text-align:right}
+.desc td{text-align:left}
+.r{text-align:right}
+.l{text-align:left}
+.pos{line-height:0em;text-align:center}
+.uno{font-size:2em;color:red}
+.dos{font-size:1.6em;color:orange}
+.tres{font-size:1.3em;color:yellow}
+.etc{color:grey}
+#foot{margin:1em;font-size:.9em}
+#foot a{color:#999}
+@media (max-width:600px){
+.t1{font-size:1.6em}
+.t2{font-size:.9em}
+h2{font-size:1.2em}
+table{font-size:1.1em}
+table.desc{font-size:.9em}
+th,td{padding:5px}
+td a{display:block;padding:5px}
+.uno{font-size:1.6em}
+.dos{font-size:1.4em}
+.tres{font-size:1.2em}
+#boards th{font-size:.8em}
+.long{word-break:break-all}
+#foot{font-size:12px}
+}
+</style>
+</head>
+<body>
+<div id="title">
+ <div class="t1"><a href="/" style="font-weight:900">Bienvenido a Internet</a> Estadísticas</div>
+ <div class="t2">
+ <span>Última actualización:</span> <span>${timestamp} GMT${tz}</span>
+ <?py if not regenerated: ?>
+ <span>(en caché)</span>
+ <?py #endif ?>
+ </div>
+</div>
+
+<hr />
+
+<h2 class="rot">Mensajes totales <span>(última semana)</span></h2>
+ <table>
+ <tr>
+ <th>D&iacute;a</th>
+ <th class="r">Hilos</th>
+ <th class="r">Resp.</th>
+ <th class="r">Total</th>
+ </tr>
+ <?py allthreads, allposts = 0, 0 ?>
+ <?py for day, posts, threads in reversed(days): ?>
+ <tr>
+ <td>${day}</td>
+ <td class="r">${threads}</td>
+ <td class="r">${int(posts)-int(threads)}</td>
+ <td class="r">${posts}</td>
+ </tr>
+ <?py allthreads += int(threads) ?>
+ <?py allposts += int(posts) ?>
+ <?py #endfor ?>
+ <tr style="font-weight:bold;">
+ <td class="l">Total</td>
+ <td class="r">${allthreads}</td>
+ <td class="r">${allposts-allthreads}</td>
+ <td class="r">${allposts}</td>
+ </tr>
+ </table>
+
+<hr />
+
+<h2 class="rot">Volumen de mensajes por sección <span>(últimos 30 días)</span></h2>
+ <table id="boards">
+ <tr>
+ <th class="pos">#</th>
+ <th class="l">Sección</th>
+ <th>Mensajes</th>
+ <th>Porcentaje</th>
+ </tr>
+ <?py iter = 1 ?>
+ <?py for dir, board, percent, num in boards_percent: ?>
+ <tr>
+ <td class="pos">
+ <?py if iter == 1: ?><span class="uno">${iter}</span>
+ <?py elif iter == 2: ?><span class="dos">${iter}</span>
+ <?py elif iter == 3: ?><span class="tres">${iter}</span>
+ <?py else: ?>${iter}<?py #endif ?>
+ </td>
+ <td class="l boards"><a href="/${dir}/" target="_blank">${board}</a></td>
+ <td class="r">${num}</td>
+ <td class="r">${percent}%</td>
+ </tr>
+ <?py iter += 1 ?>
+ <?py #endfor ?>
+ </table>
+
+<hr />
+
+<h2>Sistema</h2>
+ <table class="desc">
+ <tr><th>Máquina</th>
+ <td>maria (Debian GNU/Linux)</td></tr>
+ <tr><th>OS</th>
+ <td class="long">${uname[0]} ${uname[2]}</td></tr>
+ <tr><th>Release/Arq.</th>
+ <td>${uname[3]} ${uname[4]}</td></tr>
+ <tr><th>Motor BBS/IB</th>
+ <td>weabot ${weabot_ver}</td></tr>
+ <tr><th>Templating</th>
+ <td>tenjin ${tenjin_ver}</td></tr>
+ <tr><th>Versión de Python</th>
+ <td>${python_ver}</td></tr>
+ <tr><th>Implementación</th>
+ <td>${python_impl}</td></tr>
+ <tr><th>Build</th>
+ <td>${python_build}</td></tr>
+ <tr><th>Compilado en</th>
+ <td>${python_compiler}</td></tr>
+ </table>
+
+<hr />
+
+<h2>Base de datos</h2>
+ <table class="desc">
+ <tr><th>Base de Datos</th>
+ <td>MariaDB</td></tr>
+ <tr><th>Versión</th>
+ <td>${mysql_ver} Linux x86_64</td></tr>
+ <tr><th>Mensajes totales activos</th>
+ <td>${total}</td></tr>
+ <tr><th>Archivos totales activos</th>
+ <td>${total_files}</td></tr>
+ <tr><th>Mensajes totales archivados (BBS)</th>
+ <td>${total_archived}</td></tr>
+ <tr><th>Mensajes totales archivados (IB)</th>
+ <td>0 (QEPD)</td></tr>
+ </table>
+
+<hr />
+
+<div id="foot">B.a.I. - 2010-2019 · Contacto: <a href="mailto:burocracia@bienvenidoainternet.org">burocracia@bienvenidoainternet.org</a></div>
+</body>
+</html>
diff --git a/cgi/templates/txt_archive.html b/cgi/templates/txt_archive.html
new file mode 100644
index 0000000..b1e25db
--- /dev/null
+++ b/cgi/templates/txt_archive.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>#{threads[0]['subject']} - Archivo de #{board_name}@Bienvenido a Internet BBS</title>
+ <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" />
+<?py if threads: ?>
+ <meta property="og:site_name" content="Bienvenido a Internet BBS" />
+ <meta property="twitter:site" content="Bienvenido a Internet BBS" />
+ <meta name="description" content="${preview}" />
+ <meta property="og:title" content="${threads[0]['subject']} - ${board_name}" />
+ <meta property="og:description" content="${preview}" />
+ <meta property="twitter:title" content="${threads[0]['subject']} - ${board_name}" />
+ <meta name="twitter:description" content="${preview}" />
+<?py #endif ?>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="shortcut icon" href="#{static_url}img/favicon.ico" />
+ <link rel="stylesheet" href="/static/css/txt/bbs.css" />
+<?py if not force_css: ?>
+ <link rel="stylesheet" id="css" href="#{static_url}css/txt/#{txt_styles[txt_styles_default].lower()}.css" />
+<?py else: ?>
+ <link rel="stylesheet" type="text/css" href="#{force_css}" />
+<?py #endif ?>
+<?py if board in ['zonavip', 'world']: ?>
+ <link rel="stylesheet" href="/static/css/txt/sjis.css" />
+<?py #endif ?>
+ <script type="text/javascript" src="#{static_url}js/weabotxt.js"></script>
+ <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script>
+</head>
+<body class="threadpage archived" data-brd="#{board}">
+<?py if threads: ?>
+<?py for thread in threads: ?>
+<div id="thread_nav">
+ <a href="/" name="top" target="_top">Bienvenido a Internet</a>
+ <a href="#{boards_url}#{board}/">â– Volver al BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a>
+ <?py if thread['length'] > 100: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">1-</a>
+ <?py #endif ?>
+ <?py for i in range(thread['length'] / 100): ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{(i+1)*100+1}-#{(i+2)*100}">#{(i+1)*100+1}-</a>
+ <?py #endfor ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a>
+ <?py #endif ?>
+ <a href="#bottom">&#9660;Bajar&#9660;</a>
+</div>
+<hr /><div class="stop red">â–  Este hilo se encuentra guardado en el archivo</div><hr />
+<div class="thread" data-length="#{thread['length']}">
+ <h3>#{thread['subject']} <span>(${(str(thread['length'])+" respuestas") if thread['length'] > 1 else "Una respuesta"})</span></h3>
+ <?py for post in thread['posts']: ?>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <h4 class="deleted">#{post['num']} : Mensaje eliminado por el usuario.</h4>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <h4 class="deleted">#{post['num']} : Mensaje eliminado por miembro del staff.</h4>
+ <?py else: ?>
+ <?py if post['num'] == 1: ?>
+ <div class="reply first" data-n="#{post['num']}">
+ <?py else: ?>
+ <div class="reply" data-n="#{post['num']}">
+ <?py #endif ?>
+ <h4>#{post['num']} :
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?> : <span class="date">#{post['timestamp_formatted']}</span></h4>
+ <div class="msg">#{post['message']}</div>
+ </div>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <?py if 'size' in thread: ?>
+ <div class="size">#{thread['size']}</div>
+ <?py #endif ?>
+</div>
+<hr /><div class="stop red">â–  Este hilo se encuentra guardado en el archivo</div><hr />
+<form class="threadlinks">
+ <a href="#{boards_url}#{board}/">â– Volver al BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a>
+ <?py if prevrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Anteriores 100</a>
+ <?py #endif ?>
+ <?py if nextrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Próximos 100</a>
+ <?py #endif ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a>
+ <?py #endif ?>
+ <a href="#top">&#9650;Subir&#9650;</a>
+</form>
+<?py #endfor ?>
+<?py #endif ?>
+<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div>
+<a name="bottom"></a>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_base_top.html b/cgi/templates/txt_base_top.html
new file mode 100644
index 0000000..eb3c37b
--- /dev/null
+++ b/cgi/templates/txt_base_top.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<?py if replythread and threads: ?>
+ <title>#{threads[0]['subject']} - #{board_name}@Bienvenido a Internet BBS</title>
+<?py elif board: ?>
+ <title>#{board_long}</title>
+<?py else: ?>
+ <title>#{title}</title>
+<?py #endif ?>
+ <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=utf-8" />
+<?py if replythread and threads: ?>
+ <meta property="og:site_name" content="Bienvenido a Internet BBS" />
+ <meta property="twitter:site" content="Bienvenido a Internet BBS" />
+ <meta name="description" content="${preview}" />
+ <meta property="og:title" content="${threads[0]['subject']} - ${board_name}" />
+ <meta property="og:description" content="${preview}" />
+ <meta property="twitter:title" content="${threads[0]['subject']} - ${board_name}" />
+ <meta name="twitter:description" content="${preview}" />
+<?py #endif ?>
+ <meta name="robots" content="#{"noindex" if noindex else "index, follow"}" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="shortcut icon" href="/favicon.ico" />
+ <link rel="stylesheet" href="/static/css/txt/bbs.css" />
+<?py if not force_css: ?>
+ <link rel="stylesheet" id="css" href="#{static_url}css/txt/#{txt_styles[txt_styles_default].lower()}.css" />
+<?py else: ?>
+ <link rel="stylesheet" type="text/css" href="#{force_css}" />
+<?py #endif ?>
+<?py if board in ['zonavip', 'world']: ?>
+ <link rel="stylesheet" href="/static/css/txt/sjis.css" />
+<?py #endif ?>
+<?py if board == 'polka': ?>
+ <script type="text/javascript" src="#{static_url}js/weabotxt.test.js"></script>
+<?py else: ?>
+ <script type="text/javascript" src="#{static_url}js/weabotxt.js"></script>
+<?py #endif ?>
+ <script type="text/javascript" src="#{static_url}js/aquiencitas.js"></script>
+ <script type="text/javascript" src="#{static_url}js/shobon.js"></script>
+<?py if replythread and board != 'polka': ?>
+ <script type="text/javascript" src="#{static_url}js/autorefresh.js"></script>
+<?py #endif ?>
+</head>
diff --git a/cgi/templates/txt_board.en.html b/cgi/templates/txt_board.en.html
new file mode 100644
index 0000000..8e3c421
--- /dev/null
+++ b/cgi/templates/txt_board.en.html
@@ -0,0 +1,137 @@
+<?py include('templates/txt_base_top.html') ?>
+<body class="mainpage" data-brd="#{board}">
+<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div>
+<?py if banner_url: ?>
+ <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" />
+<?py #endif ?>
+<div id="titlebox" class="outerbox">
+ <div class="innerbox">
+ <div class="threadnav"><a href="#menu" title="Thread list">&#9632;</a><a href="#1" title="Next thread">&#9660;</a></div>
+ <h1>#{board_long}</h1>
+ <?py if postarea_desc: ?>
+ <div id="rules">#{postarea_desc}</div>
+ <?py #endif ?>
+ <form method="get" action="/tools/search.py" id="search"><input type="text" name="q" value="" /><input type="hidden" name="board" value="#{board}" /><input type="submit" value="Search active posts" /><input type="submit" value="Search archives" formaction="/tools/search_kako.py" /></form>
+ </div>
+ <div class="innerbox links"><a href="/guia.html"><b>C&oacute;mo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a>
+ <?py if not force_css: ?>| <b>Styles:</b>
+ <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?>
+ <?py #endif ?></div>
+</div>
+<?py if postarea_extra: ?>
+<div class="outerbox"><div class="innerbox">#{postarea_extra}</div></div>
+<?py #endif ?>
+<a name="menu"></a>
+<?py if threads: ?>
+<div id="threadbox" class="outerbox"><div class="innerbox">
+ <div id="threadlinks"><a href="#{cgi_url}threadlist/#{board}"><b>View all threads</b></a> <a href="kako/"><b>View archive</b></a> <a href="#newthread"><b>Create new thread</b></a></div>
+ <div id="threadlist">
+ <?py iter = 1 ?>
+ <?py for thread in threads: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: </a><a href="##{iter}"> <b>#{thread['posts'][0]['subject']}</b> (#{thread['length']})</a><br />
+ <?py iter += 1 ?>
+ <?py #endfor ?>
+ <?py for thread in more_threads: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: <b>#{thread["subject"]}</b> (#{thread["length"]})</a><br />
+ <?py iter += 1 ?>
+ <?py #endfor ?>
+ </div>
+</div></div>
+<?py titer = 1 ?>
+<?py for thread in threads: ?>
+<a name="#{titer}"></a>
+<div class="thread"><div class="innerbox">
+<div class="threadnav"><a href="#menu" title="Thread list">&#9632;</a><a href="##{(titer-1) if titer>1 else len(threads)}" title="Previous thread">&#9650;</a><a href="##{(titer+1) if titer<len(threads) else '1'}" title="Next thread">&#9660;</a></div>
+<h2><small>[#{titer}:#{thread['length']}]</small><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{thread['posts'][0]['subject']}</a></h2>
+<?py for post in thread['posts']: ?>
+<?py if post['IS_DELETED'] == '1': ?>
+ <h4 class="deleted">#{post['num']} : Post deleted by user.</h4>
+<?py elif post['IS_DELETED'] == '2': ?>
+ <h4 class="deleted">#{post['num']} : Post deleted by staff.</h4>
+<?py else: ?>
+ <div class="reply#{' first' if post['num'] == 1 else ''}" data-n="#{post['num']}">
+ <h4>#{post['num']} :
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span></h4>
+ <?py if post['file']: ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a>
+ <?py #endif ?>
+ <div class="msg">
+ #{post['message']}
+ <?py if post['shortened']: ?>
+ <div class="abbrev">(Post is too long... Click <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{post['num']}">here</a> to view the whole post.)</div>
+ <?py #endif ?>
+ </div>
+</div>
+<?py #endif ?>
+<?py #endfor ?>
+<?py if thread['locked'] != '1': ?>
+<form id="postform#{thread['id']}" class="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" />
+ <div style="display:none"><input type="text" name="name" size="15" /> <input type="text" name="email" size="15" /></div>
+ <span><input type="submit" value="Reply" /></span> <span><span>Name:&nbsp;</span><input type="text" name="fielda" size="15" /><span>&nbsp;E-mail:&nbsp;</span><input type="text" name="fieldb" size="15" /></span><br />
+ <div class="formpad">
+ <textarea name="message" cols="70" rows="5"></textarea>
+ <?py if allow_image_replies: ?><br /><input type="file" name="file" /><?py #endif ?>
+<?py else: ?>
+<form class="postform"><div class="locked">This thread has been closed. You cannot post in it any longer.</div><div class="formpad">
+<?py #endif ?>
+ <div class="threadlinks">
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/"><b>Entire thread</b></a>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50"><b>Last 50</b></a>
+ <?py #endif ?>
+ <?py if thread['length'] > 101: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100"><b>First 100</b></a>
+ <?py #endif ?>
+ <a href="#menu"><b>Thread list</b></a>
+ <a href="#newthread"><b>New thread</b></a>
+ </div>
+</div></form>
+</div></div>
+<?py titer += 1 ?>
+<?py #endfor ?>
+<?py #endif ?>
+<a name="newthread"></a>
+<div id="createbox" class="outerbox">
+ <div class="extrabox"></div>
+ <div class="innerbox">
+ <div class="threadnav"><a href="#menu" title="Thread list">&#9632;</a></div>
+ <h5>New thread form</h5>
+ <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" />
+ <table style="max-width:600px">
+ <tr>
+ <td class="pblock">Subject:</td>
+ <td colspan="3" style="width:100%"><input type="text" name="subject" size="50" maxlength="100" /></td>
+ <td><input type="submit" value="Create new thread" /></td>
+ </tr>
+ <tr>
+ <td class="pblock">Name:</td><td><input type="text" name="fielda" /></td>
+ <td class="pblock">E-mail:</td><td><input type="text" name="fieldb" /></td>
+ <td><input type="button" name="preview" value="Preview" /></td>
+ </tr>
+ <tr id="options" style="display:none"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr>
+ <tr><td class="pblock">Body:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr>
+ <?py if allow_images: ?>
+ <tr><td class="pblock">File:</td><td colspan="4"><input type="file" name="file" /></td></tr>
+ <?py #endif ?>
+ </table>
+ <div style="display:none">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div>
+ </form>
+ </div>
+</div>
+<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />ãã‚ŒãŒBaIクオリティーï¼</center>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_board.html b/cgi/templates/txt_board.html
new file mode 100644
index 0000000..097e255
--- /dev/null
+++ b/cgi/templates/txt_board.html
@@ -0,0 +1,137 @@
+<?py include('templates/txt_base_top.html') ?>
+<body class="mainpage" data-brd="#{board}">
+<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div>
+<?py if banner_url: ?>
+ <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" />
+<?py #endif ?>
+<div id="titlebox" class="outerbox">
+ <div class="innerbox">
+ <div class="threadnav"><a href="#menu" title="Ir a lista de hilos">&#9632;</a><a href="#1" title="Ir a primer hilo">&#9660;</a></div>
+ <h1>#{board_long}</h1>
+ <?py if postarea_desc: ?>
+ <div id="rules">#{postarea_desc}</div>
+ <?py #endif ?>
+ <form method="get" action="/tools/search.py" id="search"><input type="text" name="q" value="" /><input type="hidden" name="board" value="#{board}" /><input type="submit" value="Buscar en mensajes activos" /><input type="submit" value="Buscar en archivo" formaction="/tools/search_kako.py" /></form>
+ </div>
+ <div class="innerbox links"><b>¿Eres nuevo?</b> <a href="/guia.html"><b>C&oacute;mo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a>
+ <?py if not force_css: ?>| <b>Estilo:</b>
+ <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?>
+ <?py #endif ?></div>
+</div>
+<?py if postarea_extra: ?>
+<div class="outerbox"><div class="innerbox">#{postarea_extra}</div></div>
+<?py #endif ?>
+<a name="menu"></a>
+<?py if threads: ?>
+<div id="threadbox" class="outerbox"><div class="innerbox">
+ <div id="threadlinks"><a href="#{cgi_url}threadlist/#{board}"><b>Ver todos los hilos</b></a> <a href="kako/"><b>Ver hilos archivados</b></a> <a href="#newthread"><b>Crear nuevo hilo</b></a></div>
+ <div id="threadlist">
+ <?py iter = 1 ?>
+ <?py for thread in threads: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: </a><a href="##{iter}"> <b>#{thread['posts'][0]['subject']}</b> (#{thread['length']})</a><br />
+ <?py iter += 1 ?>
+ <?py #endfor ?>
+ <?py for thread in more_threads: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{iter}: <b>#{thread["subject"]}</b> (#{thread["length"]})</a><br />
+ <?py iter += 1 ?>
+ <?py #endfor ?>
+ </div>
+</div></div>
+<?py titer = 1 ?>
+<?py for thread in threads: ?>
+<a name="#{titer}"></a>
+<div class="thread"><div class="innerbox">
+<div class="threadnav"><a href="#menu" title="Lista de hilos">&#9632;</a><a href="##{(titer-1) if titer>1 else len(threads)}" title="Hilo anterior">&#9650;</a><a href="##{(titer+1) if titer<len(threads) else '1'}" title="Hilo siguiente">&#9660;</a></div>
+<h2><span>[#{titer}:#{thread['length']}]</span><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{'l50' if thread['length'] > 50 else ''}">#{thread['posts'][0]['subject']}</a></h2>
+<?py for post in thread['posts']: ?>
+<?py if post['IS_DELETED'] == '1': ?>
+ <h4 class="deleted">#{post['num']} Mensaje eliminado por el usuario.</h4>
+<?py elif post['IS_DELETED'] == '2': ?>
+ <h4 class="deleted">#{post['num']} Mensaje eliminado por miembro del staff.</h4>
+<?py else: ?>
+ <div class="reply#{' first' if post['num'] == 1 else ''}" data-n="#{post['num']}">
+ <h4>#{post['num']} :
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span></h4>
+ <?py if post['file']: ?>
+ <a href="/#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{'/static/' if post['thumb'].startswith('mime') else ('/'+board+'/thumb/')}#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a>
+ <?py #endif ?>
+ <div class="msg">
+ #{post['message']}
+ <?py if post['shortened']: ?>
+ <div class="abbrev">(Post muy largo... Presiona <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{post['num']}">aquí</a> para verlo completo.)</div>
+ <?py #endif ?>
+ </div>
+</div>
+<?py #endif ?>
+<?py #endfor ?>
+<?py if thread['locked'] != '1': ?>
+<form id="postform#{thread['id']}" class="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" />
+ <div style="display:none"><input type="text" name="name" size="15" /> <input type="text" name="email" size="15" /></div>
+ <span><input type="submit" value="Responder" /></span> <span><span>Nombre:&nbsp;</span><input type="text" name="fielda" size="15" /><span>&nbsp;E-mail:&nbsp;</span><input type="text" name="fieldb" size="15" /></span>
+ <div class="formpad">
+ <textarea name="message" cols="70" rows="5"></textarea>
+ <?py if allow_image_replies: ?><br /><input type="file" name="file" /><?py #endif ?>
+<?py else: ?>
+<form class="postform"><div class="locked">El hilo ha sido cerrado. Ya no se puede postear en él.</div><div class="formpad">
+<?py #endif ?>
+ <div class="threadlinks">
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/"><b>Hilo completo</b></a>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50"><b>&Uacute;ltimos 50</b></a>
+ <?py #endif ?>
+ <?py if thread['length'] > 101: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100"><b>Primeros 100</b></a>
+ <?py #endif ?>
+ <a href="#menu"><b>Lista de hilos</b></a>
+ <a href="#newthread"><b>Nuevo hilo</b></a>
+ </div>
+</div></form>
+</div></div>
+<?py titer += 1 ?>
+<?py #endfor ?>
+<?py #endif ?>
+<a name="newthread"></a>
+<div id="createbox" class="outerbox">
+ <div class="extrabox"></div>
+ <div class="innerbox">
+ <div class="threadnav"><a href="#menu" title="Lista de hilos">&#9632;</a></div>
+ <h5>Formulario de nuevo hilo</h5>
+ <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" />
+ <table style="max-width:600px">
+ <tr>
+ <td class="pblock">Asunto:</td>
+ <td colspan="3" style="width:100%"><input type="text" name="subject" size="50" maxlength="100" /></td>
+ <td><input type="submit" value="Crear nuevo hilo" /></td>
+ </tr>
+ <tr>
+ <td class="pblock">Nombre:</td><td><input type="text" name="fielda" /></td>
+ <td class="pblock">E-mail:</td><td><input type="text" name="fieldb" /></td>
+ <td><input type="button" name="preview" value="Previsualizar" /></td>
+ </tr>
+ <tr id="options" style="display:none"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr>
+ <tr><td class="pblock">Mensaje:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr>
+ <?py if allow_images: ?>
+ <tr><td class="pblock">Archivo:</td><td colspan="4"><input type="file" name="file" /></td></tr>
+ <?py #endif ?>
+ </table>
+ <div style="display:none">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div>
+ </form>
+ </div>
+</div>
+<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />No se ponga sensible, baisano...</center>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_error.html b/cgi/templates/txt_error.html
new file mode 100644
index 0000000..8a16a63
--- /dev/null
+++ b/cgi/templates/txt_error.html
@@ -0,0 +1,50 @@
+<html>
+<head>
+<title>Error</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<style type="text/css">
+* {word-wrap:break-word;}
+body {margin:8px;}
+.h {font-weight:bold; font-size:large;}
+.err {color:red;}
+.sub1 {color:#00AA00;} .sub2 {color:#DD00DD;}
+blockquote {margin-left:40px; margin-right:40px;}
+ul {padding-left:40px;}
+@media(max-width:650px){
+ blockquote {margin-left:20px; margin-right:20px;}
+ ul {padding-left:20px;}
+}
+</style>
+</head>
+<body>
+<div class="h err">ERROR: #{error}</div>
+<blockquote>
+ Host <b>${info['host']}</b><br>
+ <blockquote>
+ Nombre: <b>${info['name']}</b><br>
+ E-mail: ${info['email']}<br>
+ Mensaje: <br>
+ ${info['message']}
+ </blockquote>
+</blockquote>
+<hr>
+<ul>
+ <div class="h sub1">¿No sabes qué sucede?</div>
+ <ul style="line-height:1.5;">
+ ¡Revisemos!<br>
+ <b>
+ [<a href="/guia.html">¿Eres nuevo?</a>]<br />
+ [<a href="/faq.html">Preguntas frecuentes</a>]<br />
+ [<a href="#{boards_url}#{board}">Ir a la sección</a>]<br />
+ [<a href="#{cgi_url}threadlist/#{board}">Ir a la lista de hilos</a>]<br />
+ </b>
+ </ul><br>
+ <div class="h sub2">Contacto</div>
+ <ul style="line-height:1.5;">
+ Cualquier problema con el sitio por favor hacerlo llegar al staff de BaI.<br />
+ Para ello contáctanos en la <a href="/bai/">sección de discusión</a>.
+ </ul>
+</ul>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_thread.en.html b/cgi/templates/txt_thread.en.html
new file mode 100644
index 0000000..2e811cb
--- /dev/null
+++ b/cgi/templates/txt_thread.en.html
@@ -0,0 +1,105 @@
+<?py include('templates/txt_base_top.html') ?>
+<body class="threadpage" data-brd="#{board}">
+<?py if threads: ?>
+<?py for thread in threads: ?>
+<div id="thread_nav">
+ <a href="/" name="top" target="_top">Bienvenido a Internet</a>
+ <a href="#{boards_url}#{board}/">â– Return to BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Entire thread</a>
+ <?py if thread['length'] > 100: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">First 100</a>
+ <?py #endif ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Last 50</a>
+ <?py #endif ?>
+ <a href="#bottom">&#9660;Bottom&#9660;</a>
+</div>
+<hr />
+<?py if thread['length'] > 1000: ?>
+ <div class="stop red">The thread got over 1000 posts and has been closed.</div>
+<?py elif thread['length'] > 900: ?>
+ <div class="warn yellow">The thread has reached 900 posts. When it reaches 1000 posts it will be closed.</div>
+<?py #endif ?>
+<div class="thread" data-length="#{thread['length']}">
+ <h3>#{thread['subject']} <span>(${(str(thread['length'])+" replies") if thread['length']>1 else "1 reply"})</span></h3>
+ <?py for post in thread['posts']: ?>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <h4 class="deleted">#{post['num']} : Post deleted by user.</h4>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <h4 class="deleted">#{post['num']} : Post deleted by staff.</h4>
+ <?py else: ?>
+ <?py if post['num'] == 1: ?>
+ <div class="reply first" data-n="#{post['num']}">
+ <?py else: ?>
+ <div class="reply" data-n="#{post['num']}">
+ <?py #endif ?>
+ <h4><a href="#" class="num">#{post['num']}</a> :
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span>
+ <span class="del"><a href="#{cgi_url}report/#{board}/#{post['id']}/#{post['num']}">rep</a> <a href="#">del</a></span></h4>
+ <?py if post['file']: ?>
+ <a href="#{images_url}#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{images_url}#{board}/thumb/#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a>
+ <?py #endif ?>
+ <div class="msg">
+ #{post['message']}
+ </div>
+ </div>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <div class="size">#{thread['size']}</div>
+</div>
+<hr />
+<?py if thread['locked'] != '1': ?>
+ <div class="lastposts"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{thread['length']}-n" id="n">Show new posts</a></div>
+ <hr />
+<?py #endif ?>
+<?py if thread['length'] > 1000: ?>
+ <div class="stop red">The thread got over 1000 posts and has been closed.</div>
+<?py elif thread['length'] > 950: ?>
+ <div class="warn red">The thread has reached 950 posts. When it reaches 1000 posts it will be closed.</div>
+<?py elif thread['length'] > 900: ?>
+ <div class="warn yellow">The thread has reached 900 posts. When it reaches 1000 posts it will be closed.</div>
+<?py #endif ?>
+<form id="postform#{thread['id']}" class="postform" name="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <div class="threadlinks">
+ <a href="#{boards_url}#{board}">â– Return to BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Entire thread</a>
+ <?py if prevrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Previous 100</a>
+ <?py #endif ?>
+ <?py if nextrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Next 100</a>
+ <?py #endif ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Last 50</a>
+ <?py #endif ?>
+ <a href="#top">&#9650;Top&#9650;</a>
+ </div>
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" />
+ <?py if thread['locked'] != '1': ?>
+ <div style="display:none"><input type="text" name="name" size="13" /> <input type="text" name="email" size="13" /></div>
+ <span><input type="submit" value="Responder" accesskey="z" /> <input type="button" name="preview" value="Previsualizar" /></span> <span><span>Name:&nbsp;</span><input type="text" name="fielda" size="13" accesskey="n" /><span>&nbsp;E-mail:&nbsp;</span><input type="text" name="fieldb" size="13" accesskey="e" /></span><br />
+ <textarea name="message" cols="80" rows="7" accesskey="m"></textarea><br />
+ <div id="preview#{thread['id']}" class="msg" style="display:none"></div>
+ <?py if allow_image_replies: ?>
+ <input type="file" name="file" />
+ <?py #endif ?>
+ <?py #endif ?>
+</form>
+<?py #endfor ?>
+<?py #endif ?>
+<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div>
+<a name="bottom"></a>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_thread.html b/cgi/templates/txt_thread.html
new file mode 100644
index 0000000..c438944
--- /dev/null
+++ b/cgi/templates/txt_thread.html
@@ -0,0 +1,101 @@
+<?py include('templates/txt_base_top.html') ?>
+<body class="threadpage" data-brd="#{board}">
+<?py if threads: ?>
+<?py for thread in threads: ?>
+<div id="thread_nav">
+ <a href="/" name="top" target="_top">Bienvenido a Internet</a>
+ <a href="#{boards_url}#{board}/">â– Volver al BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a>
+ <?py if thread['length'] > 100: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/-100">Primeros 100</a>
+ <?py #endif ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a>
+ <?py #endif ?>
+ <a href="#bottom">&#9660;Bajar&#9660;</a>
+</div>
+<hr />
+<?py if thread['length'] > 1000: ?>
+ <div class="stop red">El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.</div>
+<?py elif thread['length'] > 900: ?>
+ <div class="warn yellow">El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.</div>
+<?py #endif ?>
+<div class="thread" data-length="#{thread['length']}">
+ <h3>#{thread['subject']} <span>(${(str(thread['length'])+" respuestas") if thread['length']>1 else "Una respuesta"})</span></h3>
+ <?py for post in thread['posts']: ?>
+ <?py if post['IS_DELETED'] == '1': ?>
+ <h4 class="deleted">#{post['num']} : Mensaje eliminado por el usuario.</h4>
+ <?py elif post['IS_DELETED'] == '2': ?>
+ <h4 class="deleted">#{post['num']} : Mensaje eliminado por miembro del staff.</h4>
+ <?py else: ?>
+ <?py if post['num'] == 1: ?>
+ <div class="reply first" data-n="#{post['num']}">
+ <?py else: ?>
+ <div class="reply" data-n="#{post['num']}">
+ <?py #endif ?>
+ <h4><a href="#" class="num">#{post['num']}</a> :
+ <?py if post['email']: ?>
+ <?py if post['tripcode']: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span></a>
+ <?py else: ?>
+ <a href="mailto:#{post['email']}"><span class="name"><b>#{post['name']}</b></span></a>
+ <?py #endif ?>
+ <?py else: ?>
+ <?py if post['tripcode']: ?>
+ <span class="name"><b>#{post['name']}</b> #{post['tripcode']}</span>
+ <?py else: ?>
+ <span class="name"><b>#{post['name']}</b></span>
+ <?py #endif ?>
+ <?py #endif ?> : <span class="date" data-unix="#{post['timestamp']}">#{post['timestamp_formatted']}</span>
+ <span class="del"><a href="#{cgi_url}report/#{board}/#{post['id']}/#{post['num']}">rep</a> <a href="#">del</a></span></h4>
+ <?py if post['file']: ?>
+ <a href="/#{board}/src/#{post['file']}" target="_blank" class="thumb"><img src="#{'/static/' if post['thumb'].startswith('mime') else ('/'+board+'/thumb/')}#{post['thumb']}" width="#{post['thumb_width']}" height="#{post['thumb_height']}" /><div>${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}</div></a>
+ <?py #endif ?>
+ <div class="msg">#{post['message']}</div>
+ </div>
+ <?py #endif ?>
+ <?py #endfor ?>
+ <div class="size">#{thread['size']}</div>
+</div>
+<hr />
+<?py if thread['locked'] != '1': ?>
+ <div class="lastposts"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{thread['length']}-n" id="n">Ver nuevos posts</a></div>
+ <hr />
+<?py #endif ?>
+<?py if thread['length'] > 1000: ?>
+ <div class="stop red">El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.</div>
+<?py elif thread['length'] > 900: ?>
+ <div class="warn yellow">El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.</div>
+<?py #endif ?>
+<form id="postform#{thread['id']}" class="postform" name="postform" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <div class="threadlinks">
+ <a href="#{boards_url}#{board}/">â– Volver al BBSâ– </a>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/">Hilo completo</a>
+ <?py if prevrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{prevrange}">Anteriores 100</a>
+ <?py #endif ?>
+ <?py if nextrange: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/#{nextrange}">Próximos 100</a>
+ <?py #endif ?>
+ <?py if thread['length'] > 51: ?>
+ <a href="#{boards_url}#{board}/read/#{thread['timestamp']}/l50">Últimos 50</a>
+ <?py #endif ?>
+ <a href="#top">&#9650;Subir&#9650;</a>
+ </div>
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="parent" value="#{thread['id']}" /><input type="hidden" name="password" value="" />
+ <?py if thread['locked'] != '1': ?>
+ <div style="display:none"><input type="text" name="name" size="13" /> <input type="text" name="email" size="13" /></div>
+ <span><input type="submit" value="Responder" accesskey="z" /> <input type="button" name="preview" value="Previsualizar" /></span> <span><span>Nombre:&nbsp;</span><input type="text" name="fielda" size="13" accesskey="n" /><span>&nbsp;E-mail:&nbsp;</span><input type="text" name="fieldb" size="13" accesskey="e" /></span><br />
+ <textarea name="message" cols="80" rows="7" accesskey="m"></textarea><br />
+ <div id="preview#{thread['id']}" class="msg" style="display:none"></div>
+ <?py if allow_image_replies: ?>
+ <input type="file" name="file" />
+ <?py #endif ?>
+ <?py #endif ?>
+</form>
+<?py #endfor ?>
+<?py #endif ?>
+<div class="end">weabot.py ver <?py include('templates/revision.html') ?> Bienvenido a Internet BBS/IB</div>
+<a name="bottom"></a>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/templates/txt_threadlist.html b/cgi/templates/txt_threadlist.html
new file mode 100644
index 0000000..bb09df4
--- /dev/null
+++ b/cgi/templates/txt_threadlist.html
@@ -0,0 +1,67 @@
+<?py include('templates/txt_base_top.html') ?>
+<body class="threads" data-brd="#{board}">
+<div id="main_nav"><a href="/" target="_top">Bienvenido a Internet</a> | <?py include('templates/navbar.html') ?></div>
+<?py if banner_url: ?>
+ <img class="banner" src="#{banner_url}" style="width:#{banner_width}px;height:#{banner_height}px;" />
+<?py #endif ?>
+<div id="titlebox" class="outerbox">
+ <div class="innerbox"><h1>#{board_long}</h1></div>
+ <div class="innerbox links"><b>¿Eres nuevo?</b> <a href="/guia.html"><b>C&oacute;mo postear</b></a> | <a href="/faq.html"><b>Preguntas frecuentes</b></a> | <a href="/bai/"><b>Contacto</b></a>
+ <?py if not force_css: ?>| <b>Apariencia:</b>
+ <?py for title in txt_styles: ?><a href="#" class="ss">#{title}</a> <?py #endfor ?>
+ <?py #endif ?></div>
+</div>
+<a name="menu"></a>
+<div id="threadbox" class="outerbox"><div class="innerbox">
+ <div id="threadlinks"><a href="#{boards_url}#{board}/"><b>Volver al BBS</b></a> <a href="/#{board}/kako/"><b>Ver hilos archivados</b></a> <a href="#newthread"><b>Crear nuevo hilo</b></a></div>
+ <div id="listmenu">Orden: <a class="l_s" href="#">Normal</a> <a class="l_s" href="#">Edad</a> <a class="l_s" href="#">Largo</a> <a class="l_s" href="#">Rapidez</a> <a class="l_s" href="#">Aleatorio</a> / Modo: <a class="l_d" href="#">Lista</a> <a class="l_d" href="#">Malla</a> / Buscar: <input id="l_sr" style="padding:0px;width:100px;" type="text"></div>
+</div></div>
+<div id="content" class="list">
+<div id="header" class="row">
+ <div>#</div>
+ <div style="width:100%;">Asunto</div>
+ <div>Resp.</div>
+ <div class="hdate">Última respuesta</div>
+</div>
+<?py iter = 1 ?>
+<?py for thread in more_threads: ?>
+<div class="row">
+ <div class="pos">#{iter}:</div>
+ <div class="thread"><a href="#{boards_url}#{board}/read/#{thread['timestamp']}/${'l50' if int(thread['length']) > 50 else ''}">#{thread["subject"]}</a></div>
+ <div class="com">#{thread["length"]}</div>
+ <div class="date" data-unix="#{timestamps[iter-1][0]}">#{timestamps[iter-1][1]}</div>
+</div>
+<?py iter += 1 ?>
+<?py #endfor ?>
+</div>
+<a name="newthread"></a>
+<div id="createbox" class="outerbox">
+ <div class="extrabox"></div>
+ <div class="innerbox">
+ <h5>Formulario de nuevo hilo</h5>
+ <form id="postform0" action="#{cgi_url}post" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="board" value="#{board}" /><input type="hidden" name="password" value="" />
+ <table style="max-width:600px;">
+ <tr>
+ <td style="text-align:right;">Asunto:</td>
+ <td colspan="3" style="width:100%;"><input type="text" name="subject" size="50" maxlength="100" /></td>
+ <td><input type="submit" value="Crear nuevo hilo" /></td>
+ </tr>
+ <tr>
+ <td style="text-align:right;">Nombre:</td><td><input type="text" name="fielda" /></td>
+ <td style="text-align:right;">E-mail:</td><td><input type="text" name="fieldb" /></td>
+ <td><input type="button" name="preview" value="Previsualizar" /></td>
+ </tr>
+ <tr id="options" style="display:none;"><td></td><td colspan="4"><div id="preview0" class="msg"></div></td></tr>
+ <tr><td style="text-align:right;">Mensaje:</td><td colspan="4"><textarea name="message" cols="70" rows="10"></textarea></td></tr>
+ <?py if allow_images: ?>
+ <tr><td style="text-align:right;">Archivo:</td><td colspan="4"><input type="file" name="file" /></td></tr>
+ <?py #endif ?>
+ </table>
+ <div style="display:none;">Trampa: <input type="text" name="name" maxlength="50" /> <input type="text" name="email" maxlength="50" /></div>
+ </form>
+ </div>
+</div>
+<center id="footer"><a href="/" target="_top">Bienvenido a Internet BBS/IB</a> weabot.py <?py include('templates/revision.html') ?> + FastCGI + tenjin<br />No se ponga sensible, baisano...</center>
+</body>
+</html> \ No newline at end of file
diff --git a/cgi/tenjin.py b/cgi/tenjin.py
new file mode 100644
index 0000000..db8cdde
--- /dev/null
+++ b/cgi/tenjin.py
@@ -0,0 +1,2118 @@
+##
+## $Release: 1.1.1 $
+## $Copyright: copyright(c) 2007-2012 kuwata-lab.com all rights reserved. $
+## $License: MIT License $
+##
+## Permission is hereby granted, free of charge, to any person obtaining
+## a copy of this software and associated documentation files (the
+## "Software"), to deal in the Software without restriction, including
+## without limitation the rights to use, copy, modify, merge, publish,
+## distribute, sublicense, and/or sell copies of the Software, and to
+## permit persons to whom the Software is furnished to do so, subject to
+## the following conditions:
+##
+## The above copyright notice and this permission notice shall be
+## included in all copies or substantial portions of the Software.
+##
+## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+##
+
+"""Very fast and light-weight template engine based embedded Python.
+ See User's Guide and examples for details.
+ http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
+ http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
+"""
+
+__version__ = "$Release: 1.1.1 $"[10:-2]
+__license__ = "$License: MIT License $"[10:-2]
+__all__ = ('Template', 'Engine', )
+
+
+import sys, os, re, time, marshal
+from time import time as _time
+from os.path import getmtime as _getmtime
+from os.path import isfile as _isfile
+random = pickle = unquote = None # lazy import
+python3 = sys.version_info[0] == 3
+python2 = sys.version_info[0] == 2
+
+logger = None
+
+
+##
+## utilities
+##
+
+def _write_binary_file(filename, content):
+ global random
+ if random is None: from random import random
+ tmpfile = filename + str(random())[1:]
+ f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb'
+ try:
+ f.write(content)
+ finally:
+ f.close()
+ if os.path.exists(tmpfile):
+ try:
+ os.rename(tmpfile, filename)
+ except:
+ os.remove(filename) # on windows, existing file should be removed before renaming
+ os.rename(tmpfile, filename)
+
+def _read_binary_file(filename):
+ f = open(filename, 'rb')
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+codecs = None # lazy import
+
+def _read_text_file(filename, encoding=None):
+ global codecs
+ if not codecs: import codecs
+ f = codecs.open(filename, encoding=(encoding or 'utf-8'))
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+def _read_template_file(filename, encoding=None):
+ s = _read_binary_file(filename) ## binary(=str)
+ if encoding: s = s.decode(encoding) ## binary(=str) to unicode
+ return s
+
+_basestring = basestring
+_unicode = unicode
+_bytes = str
+
+def _ignore_not_found_error(f, default=None):
+ try:
+ return f()
+ except OSError, ex:
+ if ex.errno == 2: # error: No such file or directory
+ return default
+ raise
+
+def create_module(module_name, dummy_func=None, **kwargs):
+ """ex. mod = create_module('tenjin.util')"""
+ try:
+ mod = type(sys)(module_name)
+ except:
+ # The module creation above does not work for Jython 2.5.2
+ import imp
+ mod = imp.new_module(module_name)
+
+ mod.__file__ = __file__
+ mod.__dict__.update(kwargs)
+ sys.modules[module_name] = mod
+ if dummy_func:
+ exec(dummy_func.func_code, mod.__dict__)
+ return mod
+
+def _raise(exception_class, *args):
+ raise exception_class(*args)
+
+
+##
+## helper method's module
+##
+
+def _dummy():
+ global unquote
+ unquote = None
+ global to_str, escape, echo, new_cycle, generate_tostrfunc
+ global start_capture, stop_capture, capture_as, captured_as, CaptureContext
+ global _p, _P, _decode_params
+
+ def generate_tostrfunc(encode=None, decode=None):
+ """Generate 'to_str' function with encode or decode encoding.
+ ex. generate to_str() function which encodes unicode into binary(=str).
+ to_str = tenjin.generate_tostrfunc(encode='utf-8')
+ repr(to_str(u'hoge')) #=> 'hoge' (str)
+ ex. generate to_str() function which decodes binary(=str) into unicode.
+ to_str = tenjin.generate_tostrfunc(decode='utf-8')
+ repr(to_str('hoge')) #=> u'hoge' (unicode)
+ """
+ if encode:
+ if decode:
+ raise ValueError("can't specify both encode and decode encoding.")
+ else:
+ def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode):
+ """Convert val into string or return '' if None. Unicode will be encoded into binary(=str)."""
+ if _isa(val, _str): return val
+ if val is None: return ''
+ #if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str)
+ if _isa(val, _unicode):
+ return val.encode(_encode) # unicode to binary(=str)
+ return _str(val)
+ else:
+ if decode:
+ def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode):
+ """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode."""
+ #if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode
+ if _isa(val, _str):
+ return val.decode(_decode)
+ if val is None: return ''
+ if _isa(val, _unicode): return val
+ return _unicode(val)
+ else:
+ def to_str(val, _str=str, _unicode=unicode, _isa=isinstance):
+ """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is."""
+ if _isa(val, _str): return val
+ if val is None: return ''
+ if _isa(val, _unicode): return val
+ return _str(val)
+ return to_str
+
+ to_str = generate_tostrfunc(encode='utf-8') # or encode=None?
+
+ def echo(string):
+ """add string value into _buf. this is equivarent to '#{string}'."""
+ lvars = sys._getframe(1).f_locals # local variables
+ lvars['_buf'].append(string)
+
+ def new_cycle(*values):
+ """Generate cycle object.
+ ex.
+ cycle = new_cycle('odd', 'even')
+ print(cycle()) #=> 'odd'
+ print(cycle()) #=> 'even'
+ print(cycle()) #=> 'odd'
+ print(cycle()) #=> 'even'
+ """
+ def gen(values):
+ i, n = 0, len(values)
+ while True:
+ yield values[i]
+ i = (i + 1) % n
+ return gen(values).next
+
+ class CaptureContext(object):
+
+ def __init__(self, name, store_to_context=True, lvars=None):
+ self.name = name
+ self.store_to_context = store_to_context
+ self.lvars = lvars or sys._getframe(1).f_locals
+
+ def __enter__(self):
+ lvars = self.lvars
+ self._buf_orig = lvars['_buf']
+ lvars['_buf'] = _buf = []
+ lvars['_extend'] = _buf.extend
+ return self
+
+ def __exit__(self, *args):
+ lvars = self.lvars
+ _buf = lvars['_buf']
+ lvars['_buf'] = self._buf_orig
+ lvars['_extend'] = self._buf_orig.extend
+ lvars[self.name] = self.captured = ''.join(_buf)
+ if self.store_to_context and '_context' in lvars:
+ lvars['_context'][self.name] = self.captured
+
+ def __iter__(self):
+ self.__enter__()
+ yield self
+ self.__exit__()
+
+ def start_capture(varname=None, _depth=1):
+ """(obsolete) start capturing with name."""
+ lvars = sys._getframe(_depth).f_locals
+ capture_context = CaptureContext(varname, None, lvars)
+ lvars['_capture_context'] = capture_context
+ capture_context.__enter__()
+
+ def stop_capture(store_to_context=True, _depth=1):
+ """(obsolete) stop capturing and return the result of capturing.
+ if store_to_context is True then the result is stored into _context[varname].
+ """
+ lvars = sys._getframe(_depth).f_locals
+ capture_context = lvars.pop('_capture_context', None)
+ if not capture_context:
+ raise Exception('stop_capture(): start_capture() is not called before.')
+ capture_context.store_to_context = store_to_context
+ capture_context.__exit__()
+ return capture_context.captured
+
+ def capture_as(name, store_to_context=True):
+ """capture partial of template."""
+ return CaptureContext(name, store_to_context, sys._getframe(1).f_locals)
+
+ def captured_as(name, _depth=1):
+ """helper method for layout template.
+ if captured string is found then append it to _buf and return True,
+ else return False.
+ """
+ lvars = sys._getframe(_depth).f_locals # local variables
+ if name in lvars:
+ _buf = lvars['_buf']
+ _buf.append(lvars[name])
+ return True
+ return False
+
+ def _p(arg):
+ """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """
+ return '<`#%s#`>' % arg # decoded into #{...} by preprocessor
+
+ def _P(arg):
+ """ex. '<b>%s</b>' % _P("item['id']") => "<b>${item['id']}</b>" """
+ return '<`$%s$`>' % arg # decoded into ${...} by preprocessor
+
+ def _decode_params(s):
+ """decode <`#...#`> and <`$...$`> into #{...} and ${...}"""
+ global unquote
+ if unquote is None:
+ from urllib import unquote
+ dct = { 'lt':'<', 'gt':'>', 'amp':'&', 'quot':'"', '#039':"'", }
+ def unescape(s):
+ #return s.replace('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&#039;', "'").replace('&amp;', '&')
+ return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s)
+ s = to_str(s)
+ s = re.sub(r'%3C%60%23(.*?)%23%60%3E', lambda m: '#{%s}' % unquote(m.group(1)), s)
+ s = re.sub(r'%3C%60%24(.*?)%24%60%3E', lambda m: '${%s}' % unquote(m.group(1)), s)
+ s = re.sub(r'&lt;`#(.*?)#`&gt;', lambda m: '#{%s}' % unescape(m.group(1)), s)
+ s = re.sub(r'&lt;`\$(.*?)\$`&gt;', lambda m: '${%s}' % unescape(m.group(1)), s)
+ s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s)
+ s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s)
+ return s
+
+helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re)
+helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc',
+ 'start_capture', 'stop_capture', 'capture_as', 'captured_as',
+ 'not_cached', 'echo_cached', 'cache_as',
+ '_p', '_P', '_decode_params',
+ ]
+generate_tostrfunc = helpers.generate_tostrfunc
+
+
+##
+## escaped module
+##
+def _dummy():
+ global is_escaped, as_escaped, to_escaped
+ global Escaped, EscapedStr, EscapedUnicode
+ global __all__
+ __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) #'Escaped', 'EscapedStr',
+
+ class Escaped(object):
+ """marking class that object is already escaped."""
+ pass
+
+ def is_escaped(value):
+ """return True if value is marked as escaped, else return False."""
+ return isinstance(value, Escaped)
+
+ class EscapedStr(str, Escaped):
+ """string class which is marked as escaped."""
+ pass
+
+ class EscapedUnicode(unicode, Escaped):
+ """unicode class which is marked as escaped."""
+ pass
+
+ def as_escaped(s):
+ """mark string as escaped, without escaping."""
+ if isinstance(s, str): return EscapedStr(s)
+ if isinstance(s, unicode): return EscapedUnicode(s)
+ raise TypeError("as_escaped(%r): expected str or unicode." % (s, ))
+
+ def to_escaped(value):
+ """convert any value into string and escape it.
+ if value is already marked as escaped, don't escape it."""
+ if hasattr(value, '__html__'):
+ value = value.__html__()
+ if is_escaped(value):
+ #return value # EscapedUnicode should be convered into EscapedStr
+ return as_escaped(_helpers.to_str(value))
+ #if isinstance(value, _basestring):
+ # return as_escaped(_helpers.escape(value))
+ return as_escaped(_helpers.escape(_helpers.to_str(value)))
+
+escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers)
+
+
+##
+## module for html
+##
+def _dummy():
+ global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs
+ global checked, selected, disabled, nl2br, text2html, nv, js_link
+
+ #_escape_table = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
+ #_escape_pattern = re.compile(r'[&<>"]')
+ ##_escape_callable = lambda m: _escape_table[m.group(0)]
+ ##_escape_callable = lambda m: _escape_table.__get__(m.group(0))
+ #_escape_get = _escape_table.__getitem__
+ #_escape_callable = lambda m: _escape_get(m.group(0))
+ #_escape_sub = _escape_pattern.sub
+
+ #def escape_html(s):
+ # return s # 3.02
+
+ #def escape_html(s):
+ # return _escape_pattern.sub(_escape_callable, s) # 6.31
+
+ #def escape_html(s):
+ # return _escape_sub(_escape_callable, s) # 6.01
+
+ #def escape_html(s, _p=_escape_pattern, _f=_escape_callable):
+ # return _p.sub(_f, s) # 6.27
+
+ #def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable):
+ # return _sub(_callable, s) # 6.04
+
+ #def escape_html(s):
+ # s = s.replace('&', '&amp;')
+ # s = s.replace('<', '&lt;')
+ # s = s.replace('>', '&gt;')
+ # s = s.replace('"', '&quot;')
+ # return s # 5.83
+
+ def escape_html(s):
+ """Escape '&', '<', '>', '"' into '&amp;', '&lt;', '&gt;', '&quot;'."""
+ return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') # 5.72
+
+ escape_xml = escape_html # for backward compatibility
+
+ def tagattr(name, expr, value=None, escape=True):
+ """(experimental) Return ' name="value"' if expr is true value, else '' (empty string).
+ If value is not specified, expr is used as value instead."""
+ if not expr and expr != 0: return _escaped.as_escaped('')
+ if value is None: value = expr
+ if escape: value = _escaped.to_escaped(value)
+ return _escaped.as_escaped(' %s="%s"' % (name, value))
+
+ def tagattrs(**kwargs):
+ """(experimental) built html tag attribtes.
+ ex.
+ >>> tagattrs(klass='main', size=20)
+ ' class="main" size="20"'
+ >>> tagattrs(klass='', size=0)
+ ''
+ """
+ kwargs = _normalize_attrs(kwargs)
+ esc = _escaped.to_escaped
+ s = ''.join([ ' %s="%s"' % (k, esc(v)) for k, v in kwargs.iteritems() if v or v == 0 ])
+ return _escaped.as_escaped(s)
+
+ def _normalize_attrs(kwargs):
+ if 'klass' in kwargs: kwargs['class'] = kwargs.pop('klass')
+ if 'checked' in kwargs: kwargs['checked'] = kwargs.pop('checked') and 'checked' or None
+ if 'selected' in kwargs: kwargs['selected'] = kwargs.pop('selected') and 'selected' or None
+ if 'disabled' in kwargs: kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None
+ return kwargs
+
+ def checked(expr):
+ """return ' checked="checked"' if expr is true."""
+ return _escaped.as_escaped(expr and ' checked="checked"' or '')
+
+ def selected(expr):
+ """return ' selected="selected"' if expr is true."""
+ return _escaped.as_escaped(expr and ' selected="selected"' or '')
+
+ def disabled(expr):
+ """return ' disabled="disabled"' if expr is true."""
+ return _escaped.as_escaped(expr and ' disabled="disabled"' or '')
+
+ def nl2br(text):
+ """replace "\n" to "<br />\n" and return it."""
+ if not text:
+ return _escaped.as_escaped('')
+ return _escaped.as_escaped(text.replace('\n', '<br />\n'))
+
+ def text2html(text, use_nbsp=True):
+ """(experimental) escape xml characters, replace "\n" to "<br />\n", and return it."""
+ if not text:
+ return _escaped.as_escaped('')
+ s = _escaped.to_escaped(text)
+ if use_nbsp: s = s.replace(' ', ' &nbsp;')
+ #return nl2br(s)
+ s = s.replace('\n', '<br />\n')
+ return _escaped.as_escaped(s)
+
+ def nv(name, value, sep=None, **kwargs):
+ """(experimental) Build name and value attributes.
+ ex.
+ >>> nv('rank', 'A')
+ 'name="rank" value="A"'
+ >>> nv('rank', 'A', '.')
+ 'name="rank" value="A" id="rank.A"'
+ >>> nv('rank', 'A', '.', checked=True)
+ 'name="rank" value="A" id="rank.A" checked="checked"'
+ >>> nv('rank', 'A', '.', klass='error', style='color:red')
+ 'name="rank" value="A" id="rank.A" class="error" style="color:red"'
+ """
+ name = _escaped.to_escaped(name)
+ value = _escaped.to_escaped(value)
+ s = sep and 'name="%s" value="%s" id="%s"' % (name, value, name+sep+value) \
+ or 'name="%s" value="%s"' % (name, value)
+ html = kwargs and s + tagattrs(**kwargs) or s
+ return _escaped.as_escaped(html)
+
+ def js_link(label, onclick, **kwargs):
+ s = kwargs and tagattrs(**kwargs) or ''
+ html = '<a href="javascript:undefined" onclick="%s;return false"%s>%s</a>' % \
+ (_escaped.to_escaped(onclick), s, _escaped.to_escaped(label))
+ return _escaped.as_escaped(html)
+
+html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped)
+helpers.escape = html.escape_html
+helpers.html = html # for backward compatibility
+sys.modules['tenjin.helpers.html'] = html
+
+
+##
+## utility function to set default encoding of template files
+##
+_template_encoding = (None, 'utf-8') # encodings for decode and encode
+
+def set_template_encoding(decode=None, encode=None):
+ """Set default encoding of template files.
+ This should be called before importing helper functions.
+ ex.
+ ## I like template files to be unicode-base like Django.
+ import tenjin
+ tenjin.set_template_encoding('utf-8') # should be called before importing helpers
+ from tenjin.helpers import *
+ """
+ global _template_encoding
+ if _template_encoding == (decode, encode):
+ return
+ if decode and encode:
+ raise ValueError("set_template_encoding(): cannot specify both decode and encode.")
+ if not decode and not encode:
+ raise ValueError("set_template_encoding(): decode or encode should be specified.")
+ if decode:
+ Template.encoding = decode # unicode base template
+ helpers.to_str = helpers.generate_tostrfunc(decode=decode)
+ else:
+ Template.encoding = None # binary base template
+ helpers.to_str = helpers.generate_tostrfunc(encode=encode)
+ _template_encoding = (decode, encode)
+
+
+##
+## Template class
+##
+
+class TemplateSyntaxError(SyntaxError):
+
+ def build_error_message(self):
+ ex = self
+ if not ex.text:
+ return self.args[0]
+ return ''.join([
+ "%s:%s:%s: %s\n" % (ex.filename, ex.lineno, ex.offset, ex.msg, ),
+ "%4d: %s\n" % (ex.lineno, ex.text.rstrip(), ),
+ " %s^\n" % (' ' * ex.offset, ),
+ ])
+
+
+class Template(object):
+ """Convert and evaluate embedded python string.
+ See User's Guide and examples for details.
+ http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
+ http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
+ """
+
+ ## default value of attributes
+ filename = None
+ encoding = None
+ escapefunc = 'escape'
+ tostrfunc = 'to_str'
+ indent = 4
+ preamble = None # "_buf = []; _expand = _buf.expand; _to_str = to_str; _escape = escape"
+ postamble = None # "print ''.join(_buf)"
+ smarttrim = None
+ args = None
+ timestamp = None
+ trace = False # if True then '<!-- begin: file -->' and '<!-- end: file -->' are printed
+
+ def __init__(self, filename=None, encoding=None, input=None, escapefunc=None, tostrfunc=None,
+ indent=None, preamble=None, postamble=None, smarttrim=None, trace=None):
+ """Initailizer of Template class.
+
+ filename:str (=None)
+ Filename to convert (optional). If None, no convert.
+ encoding:str (=None)
+ Encoding name. If specified, template string is converted into
+ unicode object internally.
+ Template.render() returns str object if encoding is None,
+ else returns unicode object if encoding name is specified.
+ input:str (=None)
+ Input string. In other words, content of template file.
+ Template file will not be read if this argument is specified.
+ escapefunc:str (='escape')
+ Escape function name.
+ tostrfunc:str (='to_str')
+ 'to_str' function name.
+ indent:int (=4)
+ Indent width.
+ preamble:str or bool (=None)
+ Preamble string which is inserted into python code.
+ If true, '_buf = []; ' is used insated.
+ postamble:str or bool (=None)
+ Postamble string which is appended to python code.
+ If true, 'print("".join(_buf))' is used instead.
+ smarttrim:bool (=None)
+ If True then "<div>\\n#{_context}\\n</div>" is parsed as
+ "<div>\\n#{_context}</div>".
+ """
+ if encoding is not None: self.encoding = encoding
+ if escapefunc is not None: self.escapefunc = escapefunc
+ if tostrfunc is not None: self.tostrfunc = tostrfunc
+ if indent is not None: self.indent = indent
+ if preamble is not None: self.preamble = preamble
+ if postamble is not None: self.postamble = postamble
+ if smarttrim is not None: self.smarttrim = smarttrim
+ if trace is not None: self.trace = trace
+ #
+ if preamble is True: self.preamble = "_buf = []"
+ if postamble is True: self.postamble = "print(''.join(_buf))"
+ if input:
+ self.convert(input, filename)
+ self.timestamp = False # False means 'file not exist' (= Engine should not check timestamp of file)
+ elif filename:
+ self.convert_file(filename)
+ else:
+ self._reset()
+
+ def _reset(self, input=None, filename=None):
+ self.script = None
+ self.bytecode = None
+ self.input = input
+ self.filename = filename
+ if input != None:
+ i = input.find("\n")
+ if i < 0:
+ self.newline = "\n" # or None
+ elif len(input) >= 2 and input[i-1] == "\r":
+ self.newline = "\r\n"
+ else:
+ self.newline = "\n"
+ self._localvars_assignments_added = False
+
+ def _localvars_assignments(self):
+ return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc)
+
+ def before_convert(self, buf):
+ if self.preamble:
+ eol = self.input.startswith('<?py') and "\n" or "; "
+ buf.append(self.preamble + eol)
+
+ def after_convert(self, buf):
+ if self.postamble:
+ if buf and not buf[-1].endswith("\n"):
+ buf.append("\n")
+ buf.append(self.postamble + "\n")
+
+ def convert_file(self, filename):
+ """Convert file into python script and return it.
+ This is equivarent to convert(open(filename).read(), filename).
+ """
+ input = _read_template_file(filename)
+ return self.convert(input, filename)
+
+ def convert(self, input, filename=None):
+ """Convert string in which python code is embedded into python script and return it.
+
+ input:str
+ Input string to convert into python code.
+ filename:str (=None)
+ Filename of input. this is optional but recommended to report errors.
+ """
+ if self.encoding and isinstance(input, str):
+ input = input.decode(self.encoding)
+ self._reset(input, filename)
+ buf = []
+ self.before_convert(buf)
+ self.parse_stmts(buf, input)
+ self.after_convert(buf)
+ script = ''.join(buf)
+ self.script = script
+ return script
+
+ STMT_PATTERN = (r'<\?py( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S)
+
+ def stmt_pattern(self):
+ pat = self.STMT_PATTERN
+ if isinstance(pat, tuple):
+ pat = self.__class__.STMT_PATTERN = re.compile(*pat)
+ return pat
+
+ def parse_stmts(self, buf, input):
+ if not input: return
+ rexp = self.stmt_pattern()
+ is_bol = True
+ index = 0
+ for m in rexp.finditer(input):
+ mspace, code, rspace = m.groups()
+ #mspace, close, rspace = m.groups()
+ #code = input[m.start()+4+len(mspace):m.end()-len(close)-(rspace and len(rspace) or 0)]
+ text = input[index:m.start()]
+ index = m.end()
+ ## detect spaces at beginning of line
+ lspace = None
+ if text == '':
+ if is_bol:
+ lspace = ''
+ elif text[-1] == '\n':
+ lspace = ''
+ else:
+ rindex = text.rfind('\n')
+ if rindex < 0:
+ if is_bol and text.isspace():
+ lspace, text = text, ''
+ else:
+ s = text[rindex+1:]
+ if s.isspace():
+ lspace, text = s, text[:rindex+1]
+ #is_bol = rspace is not None
+ ## add text, spaces, and statement
+ self.parse_exprs(buf, text, is_bol)
+ is_bol = rspace is not None
+ #if mspace == "\n":
+ if mspace and mspace.endswith("\n"):
+ code = "\n" + (code or "")
+ #if rspace == "\n":
+ if rspace and rspace.endswith("\n"):
+ code = (code or "") + "\n"
+ if code:
+ code = self.statement_hook(code)
+ m = self._match_to_args_declaration(code)
+ if m:
+ self._add_args_declaration(buf, m)
+ else:
+ self.add_stmt(buf, code)
+ rest = input[index:]
+ if rest:
+ self.parse_exprs(buf, rest)
+ self._arrange_indent(buf)
+
+ def statement_hook(self, stmt):
+ """expand macros and parse '#@ARGS' in a statement."""
+ return stmt.replace("\r\n", "\n") # Python can't handle "\r\n" in code
+
+ def _match_to_args_declaration(self, stmt):
+ if self.args is not None:
+ return None
+ args_pattern = r'^ *#@ARGS(?:[ \t]+(.*?))?$'
+ return re.match(args_pattern, stmt)
+
+ def _add_args_declaration(self, buf, m):
+ arr = (m.group(1) or '').split(',')
+ args = []; declares = []
+ for s in arr:
+ arg = s.strip()
+ if not s: continue
+ if not re.match('^[a-zA-Z_]\w*$', arg):
+ raise ValueError("%r: invalid template argument." % arg)
+ args.append(arg)
+ declares.append("%s = _context.get('%s'); " % (arg, arg))
+ self.args = args
+ #nl = stmt[m.end():]
+ #if nl: declares.append(nl)
+ buf.append(''.join(declares) + "\n")
+
+ s = '(?:\{.*?\}.*?)*'
+ EXPR_PATTERN = (r'#\{(.*?'+s+r')\}|\$\{(.*?'+s+r')\}|\{=(?:=(.*?)=|(.*?))=\}', re.S)
+ del s
+
+ def expr_pattern(self):
+ pat = self.EXPR_PATTERN
+ if isinstance(pat, tuple):
+ self.__class__.EXPR_PATTERN = pat = re.compile(*pat)
+ return pat
+
+ def get_expr_and_flags(self, match):
+ expr1, expr2, expr3, expr4 = match.groups()
+ if expr1 is not None: return expr1, (False, True) # not escape, call to_str
+ if expr2 is not None: return expr2, (True, True) # call escape, call to_str
+ if expr3 is not None: return expr3, (False, True) # not escape, call to_str
+ if expr4 is not None: return expr4, (True, True) # call escape, call to_str
+
+ def parse_exprs(self, buf, input, is_bol=False):
+ buf2 = []
+ self._parse_exprs(buf2, input, is_bol)
+ if buf2:
+ buf.append(''.join(buf2))
+
+ def _parse_exprs(self, buf, input, is_bol=False):
+ if not input: return
+ self.start_text_part(buf)
+ rexp = self.expr_pattern()
+ smarttrim = self.smarttrim
+ nl = self.newline
+ nl_len = len(nl)
+ pos = 0
+ for m in rexp.finditer(input):
+ start = m.start()
+ text = input[pos:start]
+ pos = m.end()
+ expr, flags = self.get_expr_and_flags(m)
+ #
+ if text:
+ self.add_text(buf, text)
+ self.add_expr(buf, expr, *flags)
+ #
+ if smarttrim:
+ flag_bol = text.endswith(nl) or not text and (start > 0 or is_bol)
+ if flag_bol and not flags[0] and input[pos:pos+nl_len] == nl:
+ pos += nl_len
+ buf.append("\n")
+ if smarttrim:
+ if buf and buf[-1] == "\n":
+ buf.pop()
+ rest = input[pos:]
+ if rest:
+ self.add_text(buf, rest, True)
+ self.stop_text_part(buf)
+ if input[-1] == '\n':
+ buf.append("\n")
+
+ def start_text_part(self, buf):
+ self._add_localvars_assignments_to_text(buf)
+ #buf.append("_buf.extend((")
+ buf.append("_extend((")
+
+ def _add_localvars_assignments_to_text(self, buf):
+ if not self._localvars_assignments_added:
+ self._localvars_assignments_added = True
+ buf.append(self._localvars_assignments())
+
+ def stop_text_part(self, buf):
+ buf.append("));")
+
+ def _quote_text(self, text):
+ text = re.sub(r"(['\\\\])", r"\\\1", text)
+ text = text.replace("\r\n", "\\r\n")
+ return text
+
+ def add_text(self, buf, text, encode_newline=False):
+ if not text: return
+ use_unicode = self.encoding and python2
+ buf.append(use_unicode and "u'''" or "'''")
+ text = self._quote_text(text)
+ if not encode_newline: buf.extend((text, "''', "))
+ elif text.endswith("\r\n"): buf.extend((text[0:-2], "\\r\\n''', "))
+ elif text.endswith("\n"): buf.extend((text[0:-1], "\\n''', "))
+ else: buf.extend((text, "''', "))
+
+ _add_text = add_text
+
+ def add_expr(self, buf, code, *flags):
+ if not code or code.isspace(): return
+ flag_escape, flag_tostr = flags
+ if not self.tostrfunc: flag_tostr = False
+ if not self.escapefunc: flag_escape = False
+ if flag_tostr and flag_escape: s1, s2 = "_escape(_to_str(", ")), "
+ elif flag_tostr: s1, s2 = "_to_str(", "), "
+ elif flag_escape: s1, s2 = "_escape(", "), "
+ else: s1, s2 = "(", "), "
+ buf.extend((s1, code, s2, ))
+
+ def add_stmt(self, buf, code):
+ if not code: return
+ lines = code.splitlines(True) # keep "\n"
+ if lines[-1][-1] != "\n":
+ lines[-1] = lines[-1] + "\n"
+ buf.extend(lines)
+ self._add_localvars_assignments_to_stmts(buf)
+
+ def _add_localvars_assignments_to_stmts(self, buf):
+ if self._localvars_assignments_added:
+ return
+ for index, stmt in enumerate(buf):
+ if not re.match(r'^[ \t]*(?:\#|_buf ?= ?\[\]|from __future__)', stmt):
+ break
+ else:
+ return
+ self._localvars_assignments_added = True
+ if re.match(r'^[ \t]*(if|for|while|def|with|class)\b', stmt):
+ buf.insert(index, self._localvars_assignments() + "\n")
+ else:
+ buf[index] = self._localvars_assignments() + buf[index]
+
+
+ _START_WORDS = dict.fromkeys(('for', 'if', 'while', 'def', 'try:', 'with', 'class'), True)
+ _END_WORDS = dict.fromkeys(('#end', '#endfor', '#endif', '#endwhile', '#enddef', '#endtry', '#endwith', '#endclass'), True)
+ _CONT_WORDS = dict.fromkeys(('elif', 'else:', 'except', 'except:', 'finally:'), True)
+ _WORD_REXP = re.compile(r'\S+')
+
+ depth = -1
+
+ ##
+ ## ex.
+ ## input = r"""
+ ## if items:
+ ## _buf.extend(('<ul>\n', ))
+ ## i = 0
+ ## for item in items:
+ ## i += 1
+ ## _buf.extend(('<li>', to_str(item), '</li>\n', ))
+ ## #endfor
+ ## _buf.extend(('</ul>\n', ))
+ ## #endif
+ ## """[1:]
+ ## lines = input.splitlines(True)
+ ## block = self.parse_lines(lines)
+ ## #=> [ "if items:\n",
+ ## [ "_buf.extend(('<ul>\n', ))\n",
+ ## "i = 0\n",
+ ## "for item in items:\n",
+ ## [ "i += 1\n",
+ ## "_buf.extend(('<li>', to_str(item), '</li>\n', ))\n",
+ ## ],
+ ## "#endfor\n",
+ ## "_buf.extend(('</ul>\n', ))\n",
+ ## ],
+ ## "#endif\n",
+ ## ]
+ def parse_lines(self, lines):
+ block = []
+ try:
+ self._parse_lines(lines.__iter__(), False, block, 0)
+ except StopIteration:
+ if self.depth > 0:
+ fname, linenum, colnum, linetext = self.filename, len(lines), None, None
+ raise TemplateSyntaxError("unexpected EOF.", (fname, linenum, colnum, linetext))
+ else:
+ pass
+ return block
+
+ def _parse_lines(self, lines_iter, end_block, block, linenum):
+ if block is None: block = []
+ _START_WORDS = self._START_WORDS
+ _END_WORDS = self._END_WORDS
+ _CONT_WORDS = self._CONT_WORDS
+ _WORD_REXP = self._WORD_REXP
+ get_line = lines_iter.next
+ while True:
+ line = get_line()
+ linenum += line.count("\n")
+ m = _WORD_REXP.search(line)
+ if not m:
+ block.append(line)
+ continue
+ word = m.group(0)
+ if word in _END_WORDS:
+ if word != end_block and word != '#end':
+ if end_block is False:
+ msg = "'%s' found but corresponding statement is missing." % (word, )
+ else:
+ msg = "'%s' expected but got '%s'." % (end_block, word)
+ colnum = m.start() + 1
+ raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line))
+ return block, line, None, linenum
+ elif line.endswith(':\n') or line.endswith(':\r\n'):
+ if word in _CONT_WORDS:
+ return block, line, word, linenum
+ elif word in _START_WORDS:
+ block.append(line)
+ self.depth += 1
+ cont_word = None
+ try:
+ child_block, line, cont_word, linenum = \
+ self._parse_lines(lines_iter, '#end'+word, [], linenum)
+ block.extend((child_block, line, ))
+ while cont_word: # 'elif' or 'else:'
+ child_block, line, cont_word, linenum = \
+ self._parse_lines(lines_iter, '#end'+word, [], linenum)
+ block.extend((child_block, line, ))
+ except StopIteration:
+ msg = "'%s' is not closed." % (cont_word or word)
+ colnum = m.start() + 1
+ raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line))
+ self.depth -= 1
+ else:
+ block.append(line)
+ else:
+ block.append(line)
+ assert "unreachable"
+
+ def _join_block(self, block, buf, depth):
+ indent = ' ' * (self.indent * depth)
+ for line in block:
+ if isinstance(line, list):
+ self._join_block(line, buf, depth+1)
+ elif line.isspace():
+ buf.append(line)
+ else:
+ buf.append(indent + line.lstrip())
+
+ def _arrange_indent(self, buf):
+ """arrange indentation of statements in buf"""
+ block = self.parse_lines(buf)
+ buf[:] = []
+ self._join_block(block, buf, 0)
+
+
+ def render(self, context=None, globals=None, _buf=None):
+ """Evaluate python code with context dictionary.
+ If _buf is None then return the result of evaluation as str,
+ else return None.
+
+ context:dict (=None)
+ Context object to evaluate. If None then new dict is created.
+ globals:dict (=None)
+ Global object. If None then globals() is used.
+ _buf:list (=None)
+ If None then new list is created.
+ """
+ if context is None:
+ locals = context = {}
+ elif self.args is None:
+ locals = context.copy()
+ else:
+ locals = {}
+ if '_engine' in context:
+ context.get('_engine').hook_context(locals)
+ locals['_context'] = context
+ if globals is None:
+ globals = sys._getframe(1).f_globals
+ bufarg = _buf
+ if _buf is None:
+ _buf = []
+ locals['_buf'] = _buf
+ if not self.bytecode:
+ self.compile()
+ if self.trace:
+ _buf.append("<!-- ***** begin: %s ***** -->\n" % self.filename)
+ exec(self.bytecode, globals, locals)
+ _buf.append("<!-- ***** end: %s ***** -->\n" % self.filename)
+ else:
+ exec(self.bytecode, globals, locals)
+ if bufarg is not None:
+ return bufarg
+ elif not logger:
+ return ''.join(_buf)
+ else:
+ try:
+ return ''.join(_buf)
+ except UnicodeDecodeError, ex:
+ logger.error("[tenjin.Template] " + str(ex))
+ logger.error("[tenjin.Template] (_buf=%r)" % (_buf, ))
+ raise
+
+ def compile(self):
+ """compile self.script into self.bytecode"""
+ self.bytecode = compile(self.script, self.filename or '(tenjin)', 'exec')
+
+
+##
+## preprocessor class
+##
+
+class Preprocessor(Template):
+ """Template class for preprocessing."""
+
+ STMT_PATTERN = (r'<\?PY( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S)
+
+ EXPR_PATTERN = (r'#\{\{(.*?)\}\}|\$\{\{(.*?)\}\}|\{#=(?:=(.*?)=|(.*?))=#\}', re.S)
+
+ def add_expr(self, buf, code, *flags):
+ if not code or code.isspace():
+ return
+ code = "_decode_params(%s)" % code
+ Template.add_expr(self, buf, code, *flags)
+
+
+class TemplatePreprocessor(object):
+ factory = Preprocessor
+
+ def __init__(self, factory=None):
+ if factory is not None: self.factory = factory
+ self.globals = sys._getframe(1).f_globals
+
+ def __call__(self, input, **kwargs):
+ filename = kwargs.get('filename')
+ context = kwargs.get('context') or {}
+ globals = kwargs.get('globals') or self.globals
+ template = self.factory()
+ template.convert(input, filename)
+ return template.render(context, globals=globals)
+
+
+class TrimPreprocessor(object):
+
+ _rexp = re.compile(r'^[ \t]+<', re.M)
+ _rexp_all = re.compile(r'^[ \t]+', re.M)
+
+ def __init__(self, all=False):
+ self.all = all
+
+ def __call__(self, input, **kwargs):
+ if self.all:
+ return self._rexp_all.sub('', input)
+ else:
+ return self._rexp.sub('<', input)
+
+
+class PrefixedLinePreprocessor(object):
+
+ def __init__(self, prefix='::(?=[ \t]|$)'):
+ self.prefix = prefix
+ self.regexp = re.compile(r'^([ \t]*)' + prefix + r'(.*)', re.M)
+
+ def convert_prefixed_lines(self, text):
+ fn = lambda m: "%s<?py%s ?>" % (m.group(1), m.group(2))
+ return self.regexp.sub(fn, text)
+
+ STMT_REXP = re.compile(r'<\?py\s.*?\?>', re.S)
+
+ def __call__(self, input, **kwargs):
+ buf = []; append = buf.append
+ pos = 0
+ for m in self.STMT_REXP.finditer(input):
+ text = input[pos:m.start()]
+ stmt = m.group(0)
+ pos = m.end()
+ if text: append(self.convert_prefixed_lines(text))
+ append(stmt)
+ rest = input[pos:]
+ if rest: append(self.convert_prefixed_lines(rest))
+ return "".join(buf)
+
+
+class ParseError(Exception):
+ pass
+
+
+class JavaScriptPreprocessor(object):
+
+ def __init__(self, **attrs):
+ self._attrs = attrs
+
+ def __call__(self, input, **kwargs):
+ return self.parse(input, kwargs.get('filename'))
+
+ def parse(self, input, filename=None):
+ buf = []
+ self._parse_chunks(input, buf, filename)
+ return ''.join(buf)
+
+ CHUNK_REXP = re.compile(r'(?:^( *)<|<)!-- *#(?:JS: (\$?\w+(?:\.\w+)*\(.*?\))|/JS:?) *-->([ \t]*\r?\n)?', re.M)
+
+ def _scan_chunks(self, input, filename):
+ rexp = self.CHUNK_REXP
+ pos = 0
+ curr_funcdecl = None
+ for m in rexp.finditer(input):
+ lspace, funcdecl, rspace = m.groups()
+ text = input[pos:m.start()]
+ pos = m.end()
+ if funcdecl:
+ if curr_funcdecl:
+ raise ParseError("%s is nested in %s. (file: %s, line: %s)" % \
+ (funcdecl, curr_funcdecl, filename, _linenum(input, m.start()), ))
+ curr_funcdecl = funcdecl
+ else:
+ if not curr_funcdecl:
+ raise ParseError("unexpected '<!-- #/JS -->'. (file: %s, line: %s)" % \
+ (filename, _linenum(input, m.start()), ))
+ curr_funcdecl = None
+ yield text, lspace, funcdecl, rspace, False
+ if curr_funcdecl:
+ raise ParseError("%s is not closed by '<!-- #/JS -->'. (file: %s, line: %s)" % \
+ (curr_funcdecl, filename, _linenum(input, m.start()), ))
+ rest = input[pos:]
+ yield rest, None, None, None, True
+
+ def _parse_chunks(self, input, buf, filename=None):
+ if not input: return
+ stag = '<script'
+ if self._attrs:
+ for k in self._attrs:
+ stag = "".join((stag, ' ', k, '="', self._attrs[k], '"'))
+ stag += '>'
+ etag = '</script>'
+ for text, lspace, funcdecl, rspace, end_p in self._scan_chunks(input, filename):
+ if end_p: break
+ if funcdecl:
+ buf.append(text)
+ if re.match(r'^\$?\w+\(', funcdecl):
+ buf.extend((lspace or '', stag, 'function ', funcdecl, "{var _buf='';", rspace or ''))
+ else:
+ m = re.match(r'(.+?)\((.*)\)', funcdecl)
+ buf.extend((lspace or '', stag, m.group(1), '=function(', m.group(2), "){var _buf='';", rspace or ''))
+ else:
+ self._parse_stmts(text, buf)
+ buf.extend((lspace or '', "return _buf;};", etag, rspace or ''))
+ #
+ buf.append(text)
+
+ STMT_REXP = re.compile(r'(?:^( *)<|<)\?js(\s.*?) ?\?>([ \t]*\r?\n)?', re.M | re.S)
+
+ def _scan_stmts(self, input):
+ rexp = self.STMT_REXP
+ pos = 0
+ for m in rexp.finditer(input):
+ lspace, code, rspace = m.groups()
+ text = input[pos:m.start()]
+ pos = m.end()
+ yield text, lspace, code, rspace, False
+ rest = input[pos:]
+ yield rest, None, None, None, True
+
+ def _parse_stmts(self, input, buf):
+ if not input: return
+ for text, lspace, code, rspace, end_p in self._scan_stmts(input):
+ if end_p: break
+ if lspace is not None and rspace is not None:
+ self._parse_exprs(text, buf)
+ buf.extend((lspace, code, rspace))
+ else:
+ if lspace:
+ text += lspace
+ self._parse_exprs(text, buf)
+ buf.append(code)
+ if rspace:
+ self._parse_exprs(rspace, buf)
+ if text:
+ self._parse_exprs(text, buf)
+
+ s = r'(?:\{[^{}]*?\}[^{}]*?)*'
+ EXPR_REXP = re.compile(r'\{=(.*?)=\}|([$#])\{(.*?' + s + r')\}', re.S)
+ del s
+
+ def _get_expr(self, m):
+ code1, ch, code2 = m.groups()
+ if ch:
+ code = code2
+ escape_p = ch == '$'
+ elif code1[0] == code1[-1] == '=':
+ code = code1[1:-1]
+ escape_p = False
+ else:
+ code = code1
+ escape_p = True
+ return code, escape_p
+
+ def _scan_exprs(self, input):
+ rexp = self.EXPR_REXP
+ pos = 0
+ for m in rexp.finditer(input):
+ text = input[pos:m.start()]
+ pos = m.end()
+ code, escape_p = self._get_expr(m)
+ yield text, code, escape_p, False
+ rest = input[pos:]
+ yield rest, None, None, True
+
+ def _parse_exprs(self, input, buf):
+ if not input: return
+ buf.append("_buf+=")
+ extend = buf.extend
+ op = ''
+ for text, code, escape_p, end_p in self._scan_exprs(input):
+ if end_p:
+ break
+ if text:
+ extend((op, self._escape_text(text)))
+ op = '+'
+ if code:
+ extend((op, escape_p and '_E(' or '_S(', code, ')'))
+ op = '+'
+ rest = text
+ if rest:
+ extend((op, self._escape_text(rest)))
+ if input.endswith("\n"):
+ buf.append(";\n")
+ else:
+ buf.append(";")
+
+ def _escape_text(self, text):
+ lines = text.splitlines(True)
+ fn = self._escape_str
+ s = "\\\n".join( fn(line) for line in lines )
+ return "".join(("'", s, "'"))
+
+ def _escape_str(self, string):
+ return string.replace("\\", "\\\\").replace("'", "\\'").replace("\n", r"\n")
+
+
+def _linenum(input, pos):
+ return input[0:pos].count("\n") + 1
+
+
+JS_FUNC = r"""
+function _S(x){return x==null?'':x;}
+function _E(x){return x==null?'':typeof(x)!=='string'?x:x.replace(/[&<>"']/g,_EF);}
+var _ET={'&':"&amp;",'<':"&lt;",'>':"&gt;",'"':"&quot;","'":"&#039;"};
+function _EF(c){return _ET[c];};
+"""[1:-1]
+JS_FUNC = escaped.EscapedStr(JS_FUNC)
+
+
+
+##
+## cache storages
+##
+
+class CacheStorage(object):
+ """[abstract] Template object cache class (in memory and/or file)"""
+
+ def __init__(self):
+ self.items = {} # key: full path, value: template object
+
+ def get(self, cachepath, create_template):
+ """get template object. if not found, load attributes from cache file and restore template object."""
+ template = self.items.get(cachepath)
+ if not template:
+ dct = self._load(cachepath)
+ if dct:
+ template = create_template()
+ for k in dct:
+ setattr(template, k, dct[k])
+ self.items[cachepath] = template
+ return template
+
+ def set(self, cachepath, template):
+ """set template object and save template attributes into cache file."""
+ self.items[cachepath] = template
+ dct = self._save_data_of(template)
+ return self._store(cachepath, dct)
+
+ def _save_data_of(self, template):
+ return { 'args' : template.args, 'bytecode' : template.bytecode,
+ 'script': template.script, 'timestamp': template.timestamp }
+
+ def unset(self, cachepath):
+ """remove template object from dict and cache file."""
+ self.items.pop(cachepath, None)
+ return self._delete(cachepath)
+
+ def clear(self):
+ """remove all template objects and attributes from dict and cache file."""
+ d, self.items = self.items, {}
+ for k in d.iterkeys():
+ self._delete(k)
+ d.clear()
+
+ def _load(self, cachepath):
+ """(abstract) load dict object which represents template object attributes from cache file."""
+ raise NotImplementedError.new("%s#_load(): not implemented yet." % self.__class__.__name__)
+
+ def _store(self, cachepath, template):
+ """(abstract) load dict object which represents template object attributes from cache file."""
+ raise NotImplementedError.new("%s#_store(): not implemented yet." % self.__class__.__name__)
+
+ def _delete(self, cachepath):
+ """(abstract) remove template object from cache file."""
+ raise NotImplementedError.new("%s#_delete(): not implemented yet." % self.__class__.__name__)
+
+
+class MemoryCacheStorage(CacheStorage):
+
+ def _load(self, cachepath):
+ return None
+
+ def _store(self, cachepath, template):
+ pass
+
+ def _delete(self, cachepath):
+ pass
+
+
+class FileCacheStorage(CacheStorage):
+
+ def _load(self, cachepath):
+ if not _isfile(cachepath): return None
+ if logger: logger.info("[tenjin.%s] load cache (file=%r)" % (self.__class__.__name__, cachepath))
+ data = _read_binary_file(cachepath)
+ return self._restore(data)
+
+ def _store(self, cachepath, dct):
+ if logger: logger.info("[tenjin.%s] store cache (file=%r)" % (self.__class__.__name__, cachepath))
+ data = self._dump(dct)
+ _write_binary_file(cachepath, data)
+
+ def _restore(self, data):
+ raise NotImplementedError("%s._restore(): not implemented yet." % self.__class__.__name__)
+
+ def _dump(self, dct):
+ raise NotImplementedError("%s._dump(): not implemented yet." % self.__class__.__name__)
+
+ def _delete(self, cachepath):
+ _ignore_not_found_error(lambda: os.unlink(cachepath))
+
+
+class MarshalCacheStorage(FileCacheStorage):
+
+ def _restore(self, data):
+ return marshal.loads(data)
+
+ def _dump(self, dct):
+ return marshal.dumps(dct)
+
+
+class PickleCacheStorage(FileCacheStorage):
+
+ def __init__(self, *args, **kwargs):
+ global pickle
+ if pickle is None:
+ import cPickle as pickle
+ FileCacheStorage.__init__(self, *args, **kwargs)
+
+ def _restore(self, data):
+ return pickle.loads(data)
+
+ def _dump(self, dct):
+ dct.pop('bytecode', None)
+ return pickle.dumps(dct)
+
+
+class TextCacheStorage(FileCacheStorage):
+
+ def _restore(self, data):
+ header, script = data.split("\n\n", 1)
+ timestamp = encoding = args = None
+ for line in header.split("\n"):
+ key, val = line.split(": ", 1)
+ if key == 'timestamp': timestamp = float(val)
+ elif key == 'encoding': encoding = val
+ elif key == 'args': args = val.split(', ')
+ if encoding: script = script.decode(encoding) ## binary(=str) to unicode
+ return {'args': args, 'script': script, 'timestamp': timestamp}
+
+ def _dump(self, dct):
+ s = dct['script']
+ if dct.get('encoding') and isinstance(s, unicode):
+ s = s.encode(dct['encoding']) ## unicode to binary(=str)
+ sb = []
+ sb.append("timestamp: %s\n" % dct['timestamp'])
+ if dct.get('encoding'):
+ sb.append("encoding: %s\n" % dct['encoding'])
+ if dct.get('args') is not None:
+ sb.append("args: %s\n" % ', '.join(dct['args']))
+ sb.append("\n")
+ sb.append(s)
+ s = ''.join(sb)
+ if python3:
+ if isinstance(s, str):
+ s = s.encode(dct.get('encoding') or 'utf-8') ## unicode(=str) to binary
+ return s
+
+ def _save_data_of(self, template):
+ dct = FileCacheStorage._save_data_of(self, template)
+ dct['encoding'] = template.encoding
+ return dct
+
+
+
+##
+## abstract class for data cache
+##
+class KeyValueStore(object):
+
+ def get(self, key, *options):
+ raise NotImplementedError("%s.get(): not implemented yet." % self.__class__.__name__)
+
+ def set(self, key, value, *options):
+ raise NotImplementedError("%s.set(): not implemented yet." % self.__class__.__name__)
+
+ def delete(self, key, *options):
+ raise NotImplementedError("%s.del(): not implemented yet." % self.__class__.__name__)
+
+ def has(self, key, *options):
+ raise NotImplementedError("%s.has(): not implemented yet." % self.__class__.__name__)
+
+
+##
+## memory base data cache
+##
+class MemoryBaseStore(KeyValueStore):
+
+ def __init__(self):
+ self.values = {}
+
+ def get(self, key, original_timestamp=None):
+ tupl = self.values.get(key)
+ if not tupl:
+ return None
+ value, created_at, expires_at = tupl
+ if original_timestamp is not None and created_at < original_timestamp:
+ self.delete(key)
+ return None
+ if expires_at < _time():
+ self.delete(key)
+ return None
+ return value
+
+ def set(self, key, value, lifetime=0):
+ created_at = _time()
+ expires_at = lifetime and created_at + lifetime or 0
+ self.values[key] = (value, created_at, expires_at)
+ return True
+
+ def delete(self, key):
+ try:
+ del self.values[key]
+ return True
+ except KeyError:
+ return False
+
+ def has(self, key):
+ pair = self.values.get(key)
+ if not pair:
+ return False
+ value, created_at, expires_at = pair
+ if expires_at and expires_at < _time():
+ self.delete(key)
+ return False
+ return True
+
+
+##
+## file base data cache
+##
+class FileBaseStore(KeyValueStore):
+
+ lifetime = 604800 # = 60*60*24*7
+
+ def __init__(self, root_path, encoding=None):
+ if not os.path.isdir(root_path):
+ raise ValueError("%r: directory not found." % (root_path, ))
+ self.root_path = root_path
+ if encoding is None and python3:
+ encoding = 'utf-8'
+ self.encoding = encoding
+
+ _pat = re.compile(r'[^-.\/\w]')
+
+ def filepath(self, key, _pat1=_pat):
+ return os.path.join(self.root_path, _pat1.sub('_', key))
+
+ def get(self, key, original_timestamp=None):
+ fpath = self.filepath(key)
+ #if not _isfile(fpath): return None
+ stat = _ignore_not_found_error(lambda: os.stat(fpath), None)
+ if stat is None:
+ return None
+ created_at = stat.st_ctime
+ expires_at = stat.st_mtime
+ if original_timestamp is not None and created_at < original_timestamp:
+ self.delete(key)
+ return None
+ if expires_at < _time():
+ self.delete(key)
+ return None
+ if self.encoding:
+ f = lambda: _read_text_file(fpath, self.encoding)
+ else:
+ f = lambda: _read_binary_file(fpath)
+ return _ignore_not_found_error(f, None)
+
+ def set(self, key, value, lifetime=0):
+ fpath = self.filepath(key)
+ dirname = os.path.dirname(fpath)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+ now = _time()
+ if isinstance(value, _unicode):
+ value = value.encode(self.encoding or 'utf-8')
+ _write_binary_file(fpath, value)
+ expires_at = now + (lifetime or self.lifetime) # timestamp
+ os.utime(fpath, (expires_at, expires_at))
+ return True
+
+ def delete(self, key):
+ fpath = self.filepath(key)
+ ret = _ignore_not_found_error(lambda: os.unlink(fpath), False)
+ return ret != False
+
+ def has(self, key):
+ fpath = self.filepath(key)
+ if not _isfile(fpath):
+ return False
+ if _getmtime(fpath) < _time():
+ self.delete(key)
+ return False
+ return True
+
+
+
+##
+## html fragment cache helper class
+##
+class FragmentCacheHelper(object):
+ """html fragment cache helper class."""
+
+ lifetime = 60 # 1 minute
+ prefix = None
+
+ def __init__(self, store, lifetime=None, prefix=None):
+ self.store = store
+ if lifetime is not None: self.lifetime = lifetime
+ if prefix is not None: self.prefix = prefix
+
+ def not_cached(self, cache_key, lifetime=None):
+ """(obsolete. use cache_as() instead of this.)
+ html fragment cache helper. see document of FragmentCacheHelper class."""
+ context = sys._getframe(1).f_locals['_context']
+ context['_cache_key'] = cache_key
+ key = self.prefix and self.prefix + cache_key or cache_key
+ value = self.store.get(key)
+ if value: ## cached
+ if logger: logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, ))
+ context[key] = value
+ return False
+ else: ## not cached
+ if logger: logger.debug('[tenjin.not_cached]: %r: not cached.' % (cache_key, ))
+ if key in context: del context[key]
+ if lifetime is None: lifetime = self.lifetime
+ context['_cache_lifetime'] = lifetime
+ helpers.start_capture(cache_key, _depth=2)
+ return True
+
+ def echo_cached(self):
+ """(obsolete. use cache_as() instead of this.)
+ html fragment cache helper. see document of FragmentCacheHelper class."""
+ f_locals = sys._getframe(1).f_locals
+ context = f_locals['_context']
+ cache_key = context.pop('_cache_key')
+ key = self.prefix and self.prefix + cache_key or cache_key
+ if key in context: ## cached
+ value = context.pop(key)
+ else: ## not cached
+ value = helpers.stop_capture(False, _depth=2)
+ lifetime = context.pop('_cache_lifetime')
+ self.store.set(key, value, lifetime)
+ f_locals['_buf'].append(value)
+
+ def functions(self):
+ """(obsolete. use cache_as() instead of this.)"""
+ return (self.not_cached, self.echo_cached)
+
+ def cache_as(self, cache_key, lifetime=None):
+ key = self.prefix and self.prefix + cache_key or cache_key
+ _buf = sys._getframe(1).f_locals['_buf']
+ value = self.store.get(key)
+ if value:
+ if logger: logger.debug('[tenjin.cache_as] %r: cache found.' % (cache_key, ))
+ _buf.append(value)
+ else:
+ if logger: logger.debug('[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, ))
+ _buf_len = len(_buf)
+ yield None
+ value = ''.join(_buf[_buf_len:])
+ self.store.set(key, value, lifetime)
+
+## you can change default store by 'tenjin.helpers.fragment_cache.store = ...'
+helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore())
+helpers.not_cached = helpers.fragment_cache.not_cached
+helpers.echo_cached = helpers.fragment_cache.echo_cached
+helpers.cache_as = helpers.fragment_cache.cache_as
+helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as'))
+
+
+
+##
+## helper class to find and read template
+##
+class Loader(object):
+
+ def exists(self, filepath):
+ raise NotImplementedError("%s.exists(): not implemented yet." % self.__class__.__name__)
+
+ def find(self, filename, dirs=None):
+ #: if dirs provided then search template file from it.
+ if dirs:
+ for dirname in dirs:
+ filepath = os.path.join(dirname, filename)
+ if self.exists(filepath):
+ return filepath
+ #: if dirs not provided then just return filename if file exists.
+ else:
+ if self.exists(filename):
+ return filename
+ #: if file not found then return None.
+ return None
+
+ def abspath(self, filename):
+ raise NotImplementedError("%s.abspath(): not implemented yet." % self.__class__.__name__)
+
+ def timestamp(self, filepath):
+ raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__)
+
+ def load(self, filepath):
+ raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__)
+
+
+
+##
+## helper class to find and read files
+##
+class FileSystemLoader(Loader):
+
+ def exists(self, filepath):
+ #: return True if filepath exists as a file.
+ return os.path.isfile(filepath)
+
+ def abspath(self, filepath):
+ #: return full-path of filepath
+ return os.path.abspath(filepath)
+
+ def timestamp(self, filepath):
+ #: return mtime of file
+ return _getmtime(filepath)
+
+ def load(self, filepath):
+ #: if file exists, return file content and mtime
+ def f():
+ mtime = _getmtime(filepath)
+ input = _read_template_file(filepath)
+ mtime2 = _getmtime(filepath)
+ if mtime != mtime2:
+ mtime = mtime2
+ input = _read_template_file(filepath)
+ mtime2 = _getmtime(filepath)
+ if mtime != mtime2:
+ if logger:
+ logger.warn("[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__)
+ return input, mtime
+ #: if file not exist, return None
+ return _ignore_not_found_error(f)
+
+
+##
+##
+##
+class TemplateNotFoundError(Exception):
+ pass
+
+
+
+##
+## template engine class
+##
+
+class Engine(object):
+ """Template Engine class.
+ See User's Guide and examples for details.
+ http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
+ http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
+ """
+
+ ## default value of attributes
+ prefix = ''
+ postfix = ''
+ layout = None
+ templateclass = Template
+ path = None
+ cache = TextCacheStorage() # save converted Python code into text file
+ lang = None
+ loader = FileSystemLoader()
+ preprocess = False
+ preprocessorclass = Preprocessor
+ timestamp_interval = 1 # seconds
+
+ def __init__(self, prefix=None, postfix=None, layout=None, path=None, cache=True, preprocess=None, templateclass=None, preprocessorclass=None, lang=None, loader=None, pp=None, **kwargs):
+ """Initializer of Engine class.
+
+ prefix:str (='')
+ Prefix string used to convert template short name to template filename.
+ postfix:str (='')
+ Postfix string used to convert template short name to template filename.
+ layout:str (=None)
+ Default layout template name.
+ path:list of str(=None)
+ List of directory names which contain template files.
+ cache:bool or CacheStorage instance (=True)
+ Cache storage object to store converted python code.
+ If True, default cache storage (=Engine.cache) is used (if it is None
+ then create MarshalCacheStorage object for each engine object).
+ If False, no cache storage is used nor no cache files are created.
+ preprocess:bool(=False)
+ Activate preprocessing or not.
+ templateclass:class (=Template)
+ Template class which engine creates automatically.
+ lang:str (=None)
+ Language name such as 'en', 'fr', 'ja', and so on. If you specify
+ this, cache file path will be 'inex.html.en.cache' for example.
+ pp:list (=None)
+ List of preprocessor object which is callable and manipulates template content.
+ kwargs:dict
+ Options for Template class constructor.
+ See document of Template.__init__() for details.
+ """
+ if prefix: self.prefix = prefix
+ if postfix: self.postfix = postfix
+ if layout: self.layout = layout
+ if templateclass: self.templateclass = templateclass
+ if preprocessorclass: self.preprocessorclass = preprocessorclass
+ if path is not None: self.path = path
+ if lang is not None: self.lang = lang
+ if loader is not None: self.loader = loader
+ if preprocess is not None: self.preprocess = preprocess
+ if pp is None: pp = []
+ elif isinstance(pp, list): pass
+ elif isinstance(pp, tuple): pp = list(pp)
+ else:
+ raise TypeError("'pp' expected to be a list but got %r." % (pp,))
+ self.pp = pp
+ if preprocess:
+ self.pp.append(TemplatePreprocessor(self.preprocessorclass))
+ self.kwargs = kwargs
+ self.encoding = kwargs.get('encoding')
+ self._filepaths = {} # template_name => relative path and absolute path
+ self._added_templates = {} # templates added by add_template()
+ #self.cache = cache
+ self._set_cache_storage(cache)
+
+ def _set_cache_storage(self, cache):
+ if cache is True:
+ if not self.cache:
+ self.cache = MarshalCacheStorage()
+ elif cache is None:
+ pass
+ elif cache is False:
+ self.cache = None
+ elif isinstance(cache, CacheStorage):
+ self.cache = cache
+ else:
+ raise ValueError("%r: invalid cache object." % (cache, ))
+
+ def cachename(self, filepath):
+ #: if lang is provided then add it to cache filename.
+ if self.lang:
+ return '%s.%s.cache' % (filepath, self.lang)
+ #: return cache file name.
+ else:
+ return filepath + '.cache'
+
+ def to_filename(self, template_name):
+ """Convert template short name into filename.
+ ex.
+ >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml')
+ >>> engine.to_filename(':list')
+ 'user_list.pyhtml'
+ >>> engine.to_filename('list')
+ 'list'
+ """
+ #: if template_name starts with ':', add prefix and postfix to it.
+ if template_name[0] == ':' :
+ return self.prefix + template_name[1:] + self.postfix
+ #: if template_name doesn't start with ':', just return it.
+ return template_name
+
+ def _create_template(self, input=None, filepath=None, _context=None, _globals=None):
+ #: if input is not specified then just create empty template object.
+ template = self.templateclass(None, **self.kwargs)
+ #: if input is specified then create template object and return it.
+ if input:
+ template.convert(input, filepath)
+ return template
+
+ def _preprocess(self, input, filepath, _context, _globals):
+ #if _context is None: _context = {}
+ #if _globals is None: _globals = sys._getframe(3).f_globals
+ #: preprocess template and return result
+ #preprocessor = self.preprocessorclass(filepath, input=input)
+ #return preprocessor.render(_context, globals=_globals)
+ #: preprocesses input with _context and returns result.
+ if '_engine' not in _context:
+ self.hook_context(_context)
+ for pp in self.pp:
+ input = pp.__call__(input, filename=filepath, context=_context, globals=_globals)
+ return input
+
+ def add_template(self, template):
+ self._added_templates[template.filename] = template
+
+ def _get_template_from_cache(self, cachepath, filepath):
+ #: if template not found in cache, return None
+ template = self.cache.get(cachepath, self.templateclass)
+ if not template:
+ return None
+ assert template.timestamp is not None
+ #: if checked within a sec, skip timestamp check.
+ now = _time()
+ last_checked = getattr(template, '_last_checked_at', None)
+ if last_checked and now < last_checked + self.timestamp_interval:
+ #if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \
+ # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval))
+ return template
+ #: if timestamp of template objectis same as file, return it.
+ if template.timestamp == self.loader.timestamp(filepath):
+ template._last_checked_at = now
+ return template
+ #: if timestamp of template object is different from file, clear it
+ #cache._delete(cachepath)
+ if logger: logger.info("[tenjin.%s] cache expired (filepath=%r)" % \
+ (self.__class__.__name__, filepath))
+ return None
+
+ def get_template(self, template_name, _context=None, _globals=None):
+ """Return template object.
+ If template object has not registered, template engine creates
+ and registers template object automatically.
+ """
+ #: accept template_name such as ':index'.
+ filename = self.to_filename(template_name)
+ #: if template object is added by add_template(), return it.
+ if filename in self._added_templates:
+ return self._added_templates[filename]
+ #: get filepath and fullpath of template
+ pair = self._filepaths.get(filename)
+ if pair:
+ filepath, fullpath = pair
+ else:
+ #: if template file is not found then raise TemplateNotFoundError.
+ filepath = self.loader.find(filename, self.path)
+ if not filepath:
+ raise TemplateNotFoundError('%s: filename not found (path=%r).' % (filename, self.path))
+ #
+ fullpath = self.loader.abspath(filepath)
+ self._filepaths[filename] = (filepath, fullpath)
+ #: use full path as base of cache file path
+ cachepath = self.cachename(fullpath)
+ #: get template object from cache
+ cache = self.cache
+ template = cache and self._get_template_from_cache(cachepath, filepath) or None
+ #: if template object is not found in cache or is expired...
+ if not template:
+ ret = self.loader.load(filepath)
+ if not ret:
+ raise TemplateNotFoundError("%r: template not found." % filepath)
+ input, timestamp = ret
+ if self.pp: ## required for preprocessing
+ if _context is None: _context = {}
+ if _globals is None: _globals = sys._getframe(1).f_globals
+ input = self._preprocess(input, filepath, _context, _globals)
+ #: create template object.
+ template = self._create_template(input, filepath, _context, _globals)
+ #: set timestamp and filename of template object.
+ template.timestamp = timestamp
+ template._last_checked_at = _time()
+ #: save template object into cache.
+ if cache:
+ if not template.bytecode:
+ #: ignores syntax error when compiling.
+ try: template.compile()
+ except SyntaxError: pass
+ cache.set(cachepath, template)
+ #else:
+ # template.compile()
+ #:
+ template.filename = filepath
+ return template
+
+ def include(self, template_name, append_to_buf=True, **kwargs):
+ """Evaluate template using current local variables as context.
+
+ template_name:str
+ Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template.
+ append_to_buf:boolean (=True)
+ If True then append output into _buf and return None,
+ else return stirng output.
+
+ ex.
+ <?py include('file.pyhtml') ?>
+ #{include('file.pyhtml', False)}
+ <?py val = include('file.pyhtml', False) ?>
+ """
+ #: get local and global vars of caller.
+ frame = sys._getframe(1)
+ locals = frame.f_locals
+ globals = frame.f_globals
+ #: get _context from caller's local vars.
+ assert '_context' in locals
+ context = locals['_context']
+ #: if kwargs specified then add them into context.
+ if kwargs:
+ context.update(kwargs)
+ #: get template object with context data and global vars.
+ ## (context and globals are passed to get_template() only for preprocessing.)
+ template = self.get_template(template_name, context, globals)
+ #: if append_to_buf is true then add output to _buf.
+ #: if append_to_buf is false then don't add output to _buf.
+ if append_to_buf: _buf = locals['_buf']
+ else: _buf = None
+ #: render template and return output.
+ s = template.render(context, globals, _buf=_buf)
+ #: kwargs are removed from context data.
+ if kwargs:
+ for k in kwargs:
+ del context[k]
+ return s
+
+ def render(self, template_name, context=None, globals=None, layout=True):
+ """Evaluate template with layout file and return result of evaluation.
+
+ template_name:str
+ Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template.
+ context:dict (=None)
+ Context object to evaluate. If None then new dict is used.
+ globals:dict (=None)
+ Global context to evaluate. If None then globals() is used.
+ layout:str or Bool(=True)
+ If True, the default layout name specified in constructor is used.
+ If False, no layout template is used.
+ If str, it is regarded as layout template name.
+
+ If temlate object related with the 'template_name' argument is not exist,
+ engine generates a template object and register it automatically.
+ """
+ if context is None:
+ context = {}
+ if globals is None:
+ globals = sys._getframe(1).f_globals
+ self.hook_context(context)
+ while True:
+ ## context and globals are passed to get_template() only for preprocessing
+ template = self.get_template(template_name, context, globals)
+ content = template.render(context, globals)
+ layout = context.pop('_layout', layout)
+ if layout is True or layout is None:
+ layout = self.layout
+ if not layout:
+ break
+ template_name = layout
+ layout = False
+ context['_content'] = content
+ context.pop('_content', None)
+ return content
+
+ def hook_context(self, context):
+ #: add engine itself into context data.
+ context['_engine'] = self
+ #context['render'] = self.render
+ #: add include() method into context data.
+ context['include'] = self.include
+
+
+##
+## safe template and engine
+##
+
+class SafeTemplate(Template):
+ """Uses 'to_escaped()' instead of 'escape()'.
+ '#{...}' is not allowed with this class. Use '[==...==]' instead.
+ """
+
+ tostrfunc = 'to_str'
+ escapefunc = 'to_escaped'
+
+ def get_expr_and_flags(self, match):
+ return _get_expr_and_flags(match, "#{%s}: '#{}' is not allowed with SafeTemplate.")
+
+
+class SafePreprocessor(Preprocessor):
+
+ tostrfunc = 'to_str'
+ escapefunc = 'to_escaped'
+
+ def get_expr_and_flags(self, match):
+ return _get_expr_and_flags(match, "#{{%s}}: '#{{}}' is not allowed with SafePreprocessor.")
+
+
+def _get_expr_and_flags(match, errmsg):
+ expr1, expr2, expr3, expr4 = match.groups()
+ if expr1 is not None:
+ raise TemplateSyntaxError(errmsg % match.group(1))
+ if expr2 is not None: return expr2, (True, False) # #{...} : call escape, not to_str
+ if expr3 is not None: return expr3, (False, True) # [==...==] : not escape, call to_str
+ if expr4 is not None: return expr4, (True, False) # [=...=] : call escape, not to_str
+
+
+class SafeEngine(Engine):
+
+ templateclass = SafeTemplate
+ preprocessorclass = SafePreprocessor
+
+
+##
+## for Google App Engine
+## (should separate into individual file or module?)
+##
+
+def _dummy():
+ global memcache, _tenjin
+ memcache = _tenjin = None # lazy import of google.appengine.api.memcache
+ global GaeMemcacheCacheStorage, GaeMemcacheStore, init
+
+ class GaeMemcacheCacheStorage(CacheStorage):
+
+ lifetime = 0 # 0 means unlimited
+
+ def __init__(self, lifetime=None, namespace=None):
+ CacheStorage.__init__(self)
+ if lifetime is not None: self.lifetime = lifetime
+ self.namespace = namespace
+
+ def _load(self, cachepath):
+ key = cachepath
+ if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] load cache (key=%r)" % (key, ))
+ return memcache.get(key, namespace=self.namespace)
+
+ def _store(self, cachepath, dct):
+ dct.pop('bytecode', None)
+ key = cachepath
+ if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] store cache (key=%r)" % (key, ))
+ ret = memcache.set(key, dct, self.lifetime, namespace=self.namespace)
+ if not ret:
+ if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] failed to store cache (key=%r)" % (key, ))
+
+ def _delete(self, cachepath):
+ key = cachepath
+ memcache.delete(key, namespace=self.namespace)
+
+
+ class GaeMemcacheStore(KeyValueStore):
+
+ lifetime = 0
+
+ def __init__(self, lifetime=None, namespace=None):
+ if lifetime is not None: self.lifetime = lifetime
+ self.namespace = namespace
+
+ def get(self, key):
+ return memcache.get(key, namespace=self.namespace)
+
+ def set(self, key, value, lifetime=None):
+ if lifetime is None: lifetime = self.lifetime
+ if memcache.set(key, value, lifetime, namespace=self.namespace):
+ return True
+ else:
+ if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheStore] failed to set (key=%r)" % (key, ))
+ return False
+
+ def delete(self, key):
+ return memcache.delete(key, namespace=self.namespace)
+
+ def has(self, key):
+ if memcache.add(key, 'dummy', namespace=self.namespace):
+ memcache.delete(key, namespace=self.namespace)
+ return False
+ else:
+ return True
+
+
+ def init():
+ global memcache, _tenjin
+ if not memcache:
+ from google.appengine.api import memcache
+ if not _tenjin: import tenjin as _tenjin
+ ## avoid cache confliction between versions
+ ver = os.environ.get('CURRENT_VERSION_ID', '1.1')#.split('.')[0]
+ Engine.cache = GaeMemcacheCacheStorage(namespace=ver)
+ ## set fragment cache store
+ helpers.fragment_cache.store = GaeMemcacheStore(namespace=ver)
+ helpers.fragment_cache.lifetime = 60 # 1 minute
+ helpers.fragment_cache.prefix = 'fragment.'
+
+
+gae = create_module('tenjin.gae', _dummy,
+ os=os, helpers=helpers, Engine=Engine,
+ CacheStorage=CacheStorage, KeyValueStore=KeyValueStore)
+
+
+del _dummy
diff --git a/cgi/tor.txt b/cgi/tor.txt
new file mode 100644
index 0000000..f748b96
--- /dev/null
+++ b/cgi/tor.txt
@@ -0,0 +1,1140 @@
+102.165.54.56
+103.194.170.223
+103.208.220.122
+103.208.220.226
+103.234.220.195
+103.234.220.197
+103.236.201.110
+103.236.201.27
+103.28.52.93
+103.28.53.138
+103.3.61.114
+103.75.190.11
+103.76.180.54
+104.131.206.23
+104.192.3.226
+104.194.228.240
+104.196.43.128
+104.200.20.46
+104.218.63.72
+104.218.63.73
+104.218.63.74
+104.218.63.75
+104.218.63.76
+104.244.73.126
+104.244.74.165
+104.244.74.78
+104.244.76.13
+104.244.77.49
+104.244.77.66
+104.40.73.53
+107.155.49.126
+107.173.58.166
+107.181.161.182
+107.181.174.66
+108.85.99.10
+109.169.33.163
+109.201.133.100
+109.236.90.209
+109.69.66.98
+109.69.67.17
+109.70.100.10
+109.70.100.2
+109.70.100.3
+109.70.100.4
+109.70.100.5
+109.70.100.6
+109.70.100.7
+109.70.100.8
+109.70.100.9
+111.69.49.124
+114.32.35.232
+115.64.95.48
+118.163.74.160
+122.147.141.130
+124.109.1.207
+125.212.241.182
+128.14.136.158
+128.31.0.13
+130.149.80.199
+130.204.161.3
+136.243.102.134
+137.74.167.96
+137.74.169.241
+138.197.177.62
+139.162.10.72
+139.162.100.194
+139.162.138.14
+139.28.36.234
+139.99.96.114
+139.99.98.191
+142.93.168.48
+143.106.60.70
+143.202.161.75
+144.217.161.119
+144.217.164.104
+144.217.165.223
+144.217.166.19
+144.217.166.26
+144.217.166.59
+144.217.166.65
+144.217.60.211
+144.217.60.239
+144.217.64.46
+144.217.7.154
+144.217.7.33
+144.217.80.80
+144.217.90.68
+145.239.82.204
+145.239.91.37
+145.239.93.33
+145.249.106.102
+145.249.107.135
+149.202.170.60
+149.202.238.204
+151.73.206.187
+153.207.207.191
+154.127.60.92
+156.54.213.67
+157.157.87.22
+158.174.122.199
+158.255.7.61
+158.69.192.200
+158.69.192.239
+158.69.193.32
+158.69.201.47
+158.69.217.87
+158.69.218.78
+158.69.37.14
+160.119.249.239
+160.119.249.24
+160.119.249.240
+160.119.253.114
+160.202.162.186
+162.213.0.243
+162.213.3.221
+162.244.80.228
+162.247.74.199
+162.247.74.200
+162.247.74.201
+162.247.74.202
+162.247.74.204
+162.247.74.206
+162.247.74.213
+162.247.74.217
+162.247.74.27
+162.247.74.7
+162.247.74.74
+163.172.12.160
+163.172.151.47
+163.172.160.182
+163.172.221.204
+163.172.41.228
+163.172.66.247
+164.132.51.91
+164.132.9.199
+164.77.133.220
+166.70.15.14
+166.70.207.2
+167.114.108.152
+167.114.34.150
+167.99.42.89
+169.197.112.26
+171.233.208.235
+171.25.193.20
+171.25.193.235
+171.25.193.25
+171.25.193.77
+171.25.193.78
+172.96.118.14
+172.98.193.43
+173.14.173.227
+173.212.244.116
+173.244.209.5
+173.255.226.142
+174.18.153.201
+176.10.104.240
+176.10.107.180
+176.10.99.200
+176.10.99.201
+176.10.99.202
+176.10.99.203
+176.10.99.204
+176.10.99.205
+176.10.99.206
+176.10.99.207
+176.10.99.208
+176.10.99.209
+176.10.99.210
+176.107.179.147
+176.121.81.51
+176.126.83.211
+176.31.208.193
+176.31.45.3
+176.53.90.26
+176.58.100.98
+176.58.89.182
+176.67.168.210
+178.165.72.177
+178.17.166.146
+178.17.166.147
+178.17.166.148
+178.17.166.149
+178.17.166.150
+178.17.170.105
+178.17.170.112
+178.17.170.13
+178.17.170.135
+178.17.170.149
+178.17.170.164
+178.17.170.194
+178.17.170.196
+178.17.170.23
+178.17.170.81
+178.17.171.102
+178.17.171.114
+178.17.171.197
+178.17.171.39
+178.17.171.78
+178.17.174.10
+178.17.174.14
+178.17.174.196
+178.17.174.198
+178.17.174.229
+178.17.174.232
+178.17.174.68
+178.175.131.194
+178.175.132.209
+178.175.132.210
+178.175.132.211
+178.175.132.212
+178.175.132.213
+178.175.132.214
+178.175.132.225
+178.175.132.226
+178.175.132.227
+178.175.132.228
+178.175.132.229
+178.175.132.230
+178.175.135.100
+178.175.135.101
+178.175.135.102
+178.175.135.99
+178.175.143.155
+178.175.143.156
+178.175.143.157
+178.175.143.158
+178.175.143.163
+178.175.143.164
+178.175.143.165
+178.175.143.166
+178.175.143.234
+178.175.143.242
+178.175.148.11
+178.175.148.165
+178.175.148.224
+178.175.148.227
+178.175.148.34
+178.175.148.45
+178.20.55.16
+178.20.55.18
+178.211.45.18
+178.239.176.73
+178.32.147.150
+178.32.181.96
+178.32.181.97
+178.32.181.98
+178.32.181.99
+178.43.184.11
+178.63.97.34
+179.176.55.216
+179.43.134.154
+179.43.134.155
+179.43.134.156
+179.43.134.157
+179.43.146.230
+179.43.148.214
+179.43.151.146
+179.48.248.17
+179.48.251.188
+18.18.248.17
+18.85.192.253
+180.150.226.99
+185.10.68.123
+185.10.68.148
+185.10.68.180
+185.10.68.217
+185.10.68.225
+185.10.68.52
+185.10.68.76
+185.100.85.101
+185.100.85.132
+185.100.85.147
+185.100.85.190
+185.100.85.61
+185.100.86.100
+185.100.86.128
+185.100.86.154
+185.100.86.182
+185.100.87.206
+185.100.87.207
+185.104.120.2
+185.104.120.3
+185.104.120.4
+185.104.120.5
+185.104.120.60
+185.104.120.7
+185.106.122.188
+185.107.47.171
+185.107.47.215
+185.107.70.202
+185.107.83.71
+185.112.146.138
+185.112.254.195
+185.121.168.254
+185.125.33.114
+185.125.33.242
+185.127.25.192
+185.127.25.68
+185.129.62.62
+185.129.62.63
+185.130.104.241
+185.14.29.189
+185.147.237.8
+185.147.80.155
+185.163.45.38
+185.165.168.168
+185.165.168.229
+185.165.168.77
+185.165.169.165
+185.165.169.62
+185.165.169.71
+185.169.42.141
+185.175.208.179
+185.175.208.180
+185.177.151.34
+185.180.221.225
+185.191.204.254
+185.193.125.42
+185.198.58.198
+185.205.210.245
+185.220.100.252
+185.220.100.253
+185.220.100.254
+185.220.100.255
+185.220.101.0
+185.220.101.1
+185.220.101.12
+185.220.101.13
+185.220.101.15
+185.220.101.20
+185.220.101.21
+185.220.101.22
+185.220.101.24
+185.220.101.25
+185.220.101.26
+185.220.101.27
+185.220.101.28
+185.220.101.29
+185.220.101.3
+185.220.101.30
+185.220.101.31
+185.220.101.32
+185.220.101.33
+185.220.101.34
+185.220.101.35
+185.220.101.44
+185.220.101.45
+185.220.101.46
+185.220.101.48
+185.220.101.49
+185.220.101.5
+185.220.101.50
+185.220.101.52
+185.220.101.53
+185.220.101.54
+185.220.101.56
+185.220.101.57
+185.220.101.58
+185.220.101.6
+185.220.101.60
+185.220.101.62
+185.220.101.65
+185.220.101.66
+185.220.101.67
+185.220.101.68
+185.220.101.69
+185.220.101.7
+185.220.101.70
+185.220.102.4
+185.220.102.6
+185.220.102.7
+185.220.102.8
+185.222.202.104
+185.222.202.12
+185.222.202.125
+185.222.202.133
+185.222.202.153
+185.222.202.221
+185.222.209.87
+185.227.68.78
+185.227.82.9
+185.233.100.23
+185.234.219.111
+185.234.219.112
+185.234.219.113
+185.234.219.114
+185.234.219.115
+185.234.219.116
+185.234.219.117
+185.234.219.118
+185.234.219.119
+185.234.219.120
+185.242.113.224
+185.244.151.149
+185.248.160.21
+185.248.160.231
+185.248.160.65
+185.255.112.137
+185.31.136.244
+185.34.33.2
+185.35.138.92
+185.4.132.135
+185.4.132.183
+185.56.171.94
+185.61.149.193
+185.65.205.10
+185.65.206.154
+185.66.200.10
+185.72.244.24
+185.86.148.109
+185.86.148.90
+185.86.149.254
+185.86.151.21
+187.178.75.109
+188.127.251.63
+188.165.59.43
+188.166.56.121
+188.166.9.235
+188.214.104.146
+188.68.45.180
+189.84.21.44
+190.10.8.50
+190.105.226.81
+190.164.230.184
+190.210.98.90
+190.216.2.136
+191.114.118.98
+192.155.95.222
+192.160.102.164
+192.160.102.165
+192.160.102.166
+192.160.102.168
+192.160.102.169
+192.160.102.170
+192.195.80.10
+192.34.80.176
+192.42.116.13
+192.42.116.14
+192.42.116.15
+192.42.116.16
+192.42.116.17
+192.42.116.18
+192.42.116.19
+192.42.116.20
+192.42.116.22
+192.42.116.23
+192.42.116.24
+192.42.116.25
+192.42.116.26
+192.42.116.27
+192.42.116.28
+193.110.157.151
+193.150.121.66
+193.169.145.194
+193.169.145.202
+193.169.145.66
+193.19.118.171
+193.201.225.45
+193.29.15.223
+193.36.119.17
+193.56.29.101
+193.9.114.139
+193.9.115.24
+193.90.12.115
+193.90.12.116
+193.90.12.117
+193.90.12.118
+193.90.12.119
+194.88.143.66
+195.123.209.67
+195.123.212.75
+195.123.213.211
+195.123.216.32
+195.123.217.153
+195.123.222.135
+195.123.224.108
+195.123.227.87
+195.123.228.161
+195.123.237.251
+195.123.245.96
+195.170.63.164
+195.176.3.19
+195.176.3.20
+195.176.3.23
+195.176.3.24
+195.189.96.147
+195.206.105.217
+195.228.45.176
+195.254.134.194
+195.254.134.242
+195.254.135.76
+196.41.123.180
+197.231.221.211
+198.167.223.111
+198.167.223.133
+198.167.223.38
+198.167.223.44
+198.233.204.165
+198.46.135.18
+198.50.191.95
+198.50.200.129
+198.50.200.131
+198.71.81.66
+198.73.51.73
+198.96.155.3
+198.98.50.112
+198.98.50.201
+198.98.52.93
+198.98.54.28
+198.98.54.34
+198.98.56.149
+198.98.57.155
+198.98.57.178
+198.98.58.135
+198.98.59.240
+198.98.62.49
+199.127.226.150
+199.195.248.177
+199.195.250.77
+199.195.252.246
+199.249.230.64
+199.249.230.65
+199.249.230.66
+199.249.230.67
+199.249.230.68
+199.249.230.69
+199.249.230.70
+199.249.230.71
+199.249.230.72
+199.249.230.73
+199.249.230.74
+199.249.230.75
+199.249.230.76
+199.249.230.77
+199.249.230.78
+199.249.230.79
+199.249.230.80
+199.249.230.81
+199.249.230.82
+199.249.230.83
+199.249.230.84
+199.249.230.85
+199.249.230.86
+199.249.230.87
+199.249.230.88
+199.249.230.89
+199.87.154.255
+200.98.137.240
+200.98.146.219
+200.98.161.148
+2001:0470:000d:06dd:0011:0000:0000:beef
+2001:0470:1f04:0d9a:0000:0000:0000:0002
+2001:0470:b304:0002:0000:0000:0051:0001
+2001:0620:20d0:0000:0000:0000:0000:0019
+2001:0620:20d0:0000:0000:0000:0000:0020
+2001:0620:20d0:0000:0000:0000:0000:0023
+2001:0620:20d0:0000:0000:0000:0000:0024
+2001:067c:2608:0000:0000:0000:0000:0001
+2001:067c:289c:0000:0000:0000:0000:0020
+2001:067c:289c:0000:0000:0000:0000:0025
+2001:067c:289c:0003:0000:0000:0000:0077
+2001:067c:289c:0003:0000:0000:0000:0078
+2001:0780:0107:000b:0000:0000:0000:0085
+2001:0981:5b21:000c:0000:0000:0000:0034
+2001:0985:7aa4:0000:0000:0000:0000:0002
+2001:0bc8:272a:0000:0000:0000:0000:0001
+2001:0bc8:3c96:0100:0000:0000:0000:0082
+2001:0bc8:4700:2000:0000:0000:0000:2317
+2001:0bc8:4700:2300:0000:0000:0004:021b
+2001:0bc8:4728:1203:0000:0000:0000:0001
+2001:0bc8:472c:7507:0000:0000:0000:0001
+2001:0bc8:472c:d10c:0000:0000:0000:0001
+2001:0bf0:0666:0000:0000:0000:0000:0666
+2001:0bf7:b201:0000:0000:0000:0000:0006
+2001:0bf7:b301:0000:0000:0000:0000:0006
+2001:1af8:4700:a012:0001:0000:0000:0001
+2001:1b60:0003:0221:3132:0102:0000:0001
+2001:1b60:0003:0221:4134:0101:0000:0001
+2001:1b60:0003:0239:1003:0103:0000:0001
+2001:1b60:0003:0239:1003:0106:0000:0001
+2001:41d0:0052:0100:0000:0000:0000:112a
+2001:41d0:0052:0500:0000:0000:0000:051a
+2001:41d0:0052:0cff:0000:0000:0000:01fb
+2001:41d0:0401:3100:0000:0000:0000:7d36
+2001:41d0:0404:0200:0000:0000:0000:1124
+2001:41d0:0601:1100:0000:0000:0000:06b0
+2001:41d0:0601:1100:0000:0000:0000:09eb
+2001:41d0:0601:1100:0000:0000:0000:0eb0
+2001:41d0:0701:1100:0000:0000:0000:0761
+2001:41d0:0701:1100:0000:0000:0000:1a12
+2001:41d0:0801:2000:0000:0000:0000:0270
+2001:41d0:1008:26d8:0000:0000:0000:0150
+2001:4b78:2006:ffc3:0000:0000:0000:0001
+2001:4ba0:fff9:0160:dead:beef:ca1f:1337
+2001:b011:4010:3264:0000:0000:0000:0006
+2002:ce3f:e590:0001:0001:0000:0000:0015
+201.80.164.203
+201.80.181.11
+204.11.50.131
+204.17.56.42
+204.194.29.4
+204.209.81.3
+204.27.60.147
+204.8.156.142
+204.85.191.30
+204.85.191.9
+205.168.84.133
+205.185.126.56
+205.185.127.219
+206.248.184.127
+206.55.74.0
+207.180.224.17
+207.192.70.250
+207.244.70.35
+209.126.101.29
+209.141.33.25
+209.141.37.237
+209.141.40.86
+209.141.41.41
+209.141.45.212
+209.141.51.150
+209.141.58.114
+209.141.61.45
+209.95.51.11
+210.140.10.24
+210.3.102.152
+212.16.104.33
+212.21.66.6
+212.47.226.52
+212.47.229.60
+212.47.248.66
+212.81.199.159
+213.108.105.71
+213.136.92.52
+213.252.140.118
+213.252.244.99
+213.61.215.53
+213.95.149.22
+216.158.98.38
+216.19.178.143
+216.218.134.12
+216.239.90.19
+216.244.85.211
+217.115.10.131
+217.115.10.132
+217.12.221.196
+217.12.223.56
+217.170.197.83
+217.170.197.89
+217.182.78.177
+220.135.203.167
+223.26.48.248
+23.129.64.101
+23.129.64.104
+23.129.64.105
+23.129.64.106
+23.239.23.104
+23.94.113.11
+24.20.43.120
+24.3.111.78
+2400:8902:0000:0000:f03c:91ff:fe6b:3903
+2600:3c00:0000:0000:f03c:91ff:fee2:4963
+2600:3c01:0000:0000:f03c:91ff:fe30:ec17
+2600:3c03:0000:0000:f03c:91ff:fefa:755c
+2601:01c2:1900:f202:0c2b:7370:df29:2ffe
+2604:9a00:2010:a08d:0010:0000:0000:0023
+2605:2700:0000:0002:a800:00ff:fe20:0db3
+2605:2700:0000:0002:a800:00ff:fe39:0574
+2605:2700:0000:0002:a800:00ff:fe64:64ea
+2605:4d00:0000:0002:0000:0000:0000:006e
+2605:6400:0010:020b:226d:70ab:4c95:029b
+2605:6400:0010:0549:0000:0000:0000:0001
+2605:6400:0010:0655:a871:c796:0015:f519
+2605:6400:0020:0693:279d:170f:8868:bc3e
+2605:6400:0020:09ce:0000:0000:0000:0001
+2605:6400:0020:0e9d:2309:1a4d:8bd7:ea1c
+2605:6400:0030:fa4e:aa41:e6cb:ec4d:230a
+2605:6400:0030:fa6b:0000:0000:0000:0001
+2605:e200:d111:0001:0225:90ff:fe24:3f9e
+2605:f700:00c0:0001:0000:0000:0de9:142a
+2607:5300:0120:0e93:0000:0000:0000:0110
+2607:5300:0201:3100:0000:0000:0000:0c20
+2607:ff68:0100:0089:0000:0000:0000:0005
+2620:0007:6001:0000:0000:ffff:c759:e640
+2620:0007:6001:0000:0000:ffff:c759:e641
+2620:0007:6001:0000:0000:ffff:c759:e642
+2620:0007:6001:0000:0000:ffff:c759:e643
+2620:0007:6001:0000:0000:ffff:c759:e644
+2620:0007:6001:0000:0000:ffff:c759:e645
+2620:0007:6001:0000:0000:ffff:c759:e646
+2620:0007:6001:0000:0000:ffff:c759:e647
+2620:0007:6001:0000:0000:ffff:c759:e648
+2620:0007:6001:0000:0000:ffff:c759:e649
+2620:0007:6001:0000:0000:ffff:c759:e64a
+2620:0007:6001:0000:0000:ffff:c759:e64b
+2620:0007:6001:0000:0000:ffff:c759:e64c
+2620:0007:6001:0000:0000:ffff:c759:e64d
+2620:0007:6001:0000:0000:ffff:c759:e64e
+2620:0007:6001:0000:0000:ffff:c759:e64f
+2620:0007:6001:0000:0000:ffff:c759:e650
+2620:0007:6001:0000:0000:ffff:c759:e651
+2620:0007:6001:0000:0000:ffff:c759:e652
+2620:0007:6001:0000:0000:ffff:c759:e653
+2620:0007:6001:0000:0000:ffff:c759:e654
+2620:0007:6001:0000:0000:ffff:c759:e655
+2620:0007:6001:0000:0000:ffff:c759:e656
+2620:0007:6001:0000:0000:ffff:c759:e657
+2620:0007:6001:0000:0000:ffff:c759:e658
+2620:0007:6001:0000:0000:ffff:c759:e659
+2620:0132:300c:c01d:0000:0000:0000:0004
+2620:0132:300c:c01d:0000:0000:0000:0005
+2620:0132:300c:c01d:0000:0000:0000:0006
+2620:0132:300c:c01d:0000:0000:0000:0008
+2620:0132:300c:c01d:0000:0000:0000:0009
+2620:0132:300c:c01d:0000:0000:0000:000a
+2620:018c:0000:1001:0000:0000:0000:0101
+2620:018c:0000:1001:0000:0000:0000:0104
+2620:018c:0000:1001:0000:0000:0000:0105
+2620:018c:0000:1001:0000:0000:0000:0106
+27.102.128.26
+2a00:0c98:2030:a03e:0002:0000:0000:0a10
+2a00:1298:8011:0212:0000:0000:0000:0163
+2a00:1298:8011:0212:0000:0000:0000:0164
+2a00:1298:8011:0212:0000:0000:0000:0165
+2a00:1328:e102:8000:0000:0000:0000:0131
+2a00:1768:1001:0021:0000:0000:32a3:201a
+2a00:1768:2001:0023:1000:0000:0000:0200
+2a00:1768:6001:0016:0000:0000:0000:0071
+2a00:1dc0:2048:0000:0000:0000:0000:0002
+2a00:1dc0:cafe:0000:0000:0000:d6a2:ae67
+2a00:1dc0:cafe:0000:0000:0000:f290:7489
+2a00:1dc0:caff:0029:0000:0000:0000:6d8e
+2a00:1dc0:caff:003a:0000:0000:0000:dcbe
+2a00:1dc0:caff:0054:0000:0000:0000:a46d
+2a00:1dc0:caff:0071:0000:0000:0000:e4da
+2a00:1dc0:caff:0072:0000:0000:0000:2cb4
+2a00:1dc0:caff:007d:0000:0000:0000:8254
+2a00:1dc0:caff:008b:0000:0000:0000:5b9a
+2a00:1dc0:caff:009e:0000:0000:0000:8e67
+2a00:1dc0:caff:00b0:0000:0000:0000:93c4
+2a00:1dc0:caff:00f6:0000:0000:0000:28ad
+2a00:1dc0:caff:00f8:0000:0000:0001:c46a
+2a00:1dc0:caff:010d:0000:0000:0000:234b
+2a00:1dc0:caff:0111:0000:0000:0000:785b
+2a00:1dc0:caff:0127:0000:0000:0000:e359
+2a00:1dc0:caff:0129:0000:0000:0000:4938
+2a00:1dc0:caff:0138:0000:0000:0000:94d2
+2a00:1dc0:caff:014e:0000:0000:0000:9ecd
+2a00:1dc0:caff:0153:0000:0000:0000:d2c7
+2a00:1dc0:caff:0159:0000:0000:0000:4d79
+2a00:1dc0:caff:015c:0000:0000:0000:5627
+2a00:1dc0:caff:0168:0000:0000:0000:6b79
+2a00:5880:1801:0000:2891:33ff:fe93:d6a0
+2a01:04f9:c010:08fb:0000:0000:0000:0bee
+2a01:0e35:8be7:65f0:0043:07ff:fe82:ac61
+2a01:7e00:0000:0000:f03c:91ff:fe56:2656
+2a01:7e01:0000:0000:f03c:91ff:fe6b:575b
+2a02:0418:6017:0000:0000:0000:0000:0147
+2a02:0418:6017:0000:0000:0000:0000:0148
+2a02:0a00:2000:0034:0000:0000:0000:0195
+2a02:0ec0:0209:0010:0000:0000:0000:0004
+2a02:2970:1002:0000:5054:11ff:fe21:fb21
+2a02:2970:1002:0000:5054:45ff:fe4b:5a29
+2a02:2970:1002:0000:5054:a2ff:fed6:4d6c
+2a02:2970:1002:0000:5054:a8ff:fe63:b164
+2a02:29e0:0002:0006:0001:0001:1156:b142
+2a02:29e0:0002:0006:0001:0001:1628:58bb
+2a02:7aa0:0043:0000:0000:0000:1d04:1c97
+2a03:4000:0002:0a11:3a58:da1f:cffa:01bc
+2a03:4000:0021:047a:0de1:0ea7:dead:beef
+2a03:4000:0032:0488:08a9:72ff:fef6:07aa
+2a03:b0c0:0000:1010:0000:0000:024c:1001
+2a03:b0c0:0002:00d0:0000:0000:0db1:4001
+2a03:b0c0:0003:00d0:0000:0000:0d9a:3001
+2a03:e600:0100:0000:0000:0000:0000:0002
+2a03:e600:0100:0000:0000:0000:0000:0003
+2a03:e600:0100:0000:0000:0000:0000:0004
+2a03:e600:0100:0000:0000:0000:0000:0005
+2a03:e600:0100:0000:0000:0000:0000:0006
+2a03:e600:0100:0000:0000:0000:0000:0007
+2a03:e600:0100:0000:0000:0000:0000:0008
+2a03:e600:0100:0000:0000:0000:0000:0009
+2a03:e600:0100:0000:0000:0000:0000:000a
+2a04:9dc0:00c1:0007:0216:3eff:fe5c:3d83
+2a06:1700:0000:000b:0000:0000:44cb:00d9
+2a06:1700:0000:001f:0000:0000:0000:0031
+2a06:1700:0001:0000:0000:0000:0000:0007
+2a06:1700:0001:0000:0000:0000:0000:0011
+2a06:3000:0000:0000:0000:0000:0120:0002
+2a06:3000:0000:0000:0000:0000:0120:0003
+2a06:3000:0000:0000:0000:0000:0120:0004
+2a06:3000:0000:0000:0000:0000:0120:0005
+2a06:3000:0000:0000:0000:0000:0120:0007
+2a06:3000:0000:0000:0000:0000:0120:0060
+2a06:d380:0000:3700:0000:0000:0000:0062
+2a06:d380:0000:3700:0000:0000:0000:0063
+2a0b:f4c0:016c:0001:0000:0000:0000:0001
+2a0b:f4c0:016c:0002:0000:0000:0000:0001
+2a0b:f4c0:016c:0003:0000:0000:0000:0001
+2a0b:f4c0:016c:0004:0000:0000:0000:0001
+2a0b:f4c1:0000:0000:0000:0000:0000:0004
+2a0b:f4c1:0000:0000:0000:0000:0000:0006
+2a0b:f4c1:0000:0000:0000:0000:0000:0007
+2a0b:f4c1:0000:0000:0000:0000:0000:0008
+2a0c:b807:8000:c93a:ff51:90ac:0000:13fc
+2a0c:b807:8000:c93a:ff51:90ac:0000:1b88
+2a0c:b807:8000:c93a:ff51:90ac:0000:1bae
+2c0f:f930:0000:0003:0000:0000:0000:0221
+2c0f:f930:0000:0005:0000:0000:0000:0038
+31.131.2.19
+31.131.4.171
+31.148.220.211
+31.185.104.19
+31.185.104.20
+31.185.104.21
+31.185.27.203
+31.220.0.225
+31.220.40.54
+31.220.42.86
+31.31.72.24
+31.31.74.131
+31.31.74.47
+35.0.127.52
+37.128.222.30
+37.134.164.64
+37.139.8.104
+37.187.105.104
+37.187.180.18
+37.187.239.8
+37.200.98.117
+37.220.36.240
+37.228.129.2
+37.235.48.36
+37.28.154.68
+37.48.120.196
+37.9.231.195
+38.117.96.154
+40.124.44.53
+41.215.241.146
+45.125.65.45
+45.33.43.215
+45.35.72.85
+45.56.103.80
+45.62.250.175
+45.62.250.179
+45.64.186.102
+45.66.32.220
+45.76.115.159
+45.79.144.222
+45.79.73.22
+46.101.61.36
+46.105.52.65
+46.165.230.5
+46.165.245.154
+46.165.254.166
+46.166.139.35
+46.167.245.51
+46.17.46.199
+46.173.214.3
+46.182.106.190
+46.182.18.29
+46.182.18.40
+46.182.19.15
+46.182.19.219
+46.246.49.139
+46.250.220.166
+46.29.248.238
+46.36.36.184
+46.38.235.14
+46.4.144.81
+46.98.199.52
+46.98.200.43
+47.89.178.105
+49.50.107.221
+49.50.66.209
+5.135.158.101
+5.135.65.145
+5.150.254.67
+5.189.143.169
+5.189.146.133
+5.196.1.129
+5.196.66.162
+5.199.130.188
+5.2.64.194
+5.2.77.146
+5.200.52.112
+5.252.176.20
+5.254.146.7
+5.3.163.124
+5.34.181.34
+5.34.181.35
+5.34.183.105
+5.39.217.14
+5.45.76.56
+5.61.37.133
+5.79.68.161
+5.79.86.15
+5.79.86.16
+50.247.195.124
+50.7.151.127
+50.7.176.2
+51.15.0.226
+51.15.106.67
+51.15.117.50
+51.15.123.230
+51.15.125.181
+51.15.128.3
+51.15.187.209
+51.15.209.128
+51.15.224.0
+51.15.233.253
+51.15.235.211
+51.15.252.1
+51.15.3.40
+51.15.34.214
+51.15.36.100
+51.15.37.97
+51.15.43.205
+51.15.48.204
+51.15.49.134
+51.15.53.83
+51.15.56.18
+51.15.59.175
+51.15.59.9
+51.15.68.66
+51.15.75.133
+51.15.80.14
+51.15.92.212
+51.159.1.114
+51.254.208.245
+51.254.48.93
+51.255.106.85
+51.38.113.64
+51.38.134.189
+51.38.162.232
+51.38.64.136
+51.68.174.112
+51.68.214.45
+51.75.253.147
+51.75.71.123
+51.77.177.194
+51.77.193.218
+51.77.201.37
+51.77.62.52
+52.15.194.28
+52.167.231.173
+54.36.189.105
+54.36.222.37
+54.37.16.241
+54.37.234.66
+54.39.148.232
+54.39.148.233
+54.39.148.234
+54.39.151.167
+58.153.198.85
+59.115.159.251
+59.127.163.155
+62.102.148.67
+62.102.148.68
+62.102.148.69
+62.210.105.86
+62.210.116.201
+62.210.37.82
+64.113.32.29
+64.137.162.34
+64.27.17.140
+65.181.122.48
+65.181.123.254
+65.181.124.115
+65.19.167.130
+65.19.167.131
+65.19.167.132
+66.110.216.10
+66.146.193.33
+66.155.4.213
+66.175.208.248
+66.222.153.25
+66.42.224.235
+67.163.131.76
+67.215.255.140
+68.46.79.221
+69.162.107.5
+69.164.207.234
+70.168.93.214
+71.19.144.106
+71.19.144.148
+71.19.148.20
+72.14.179.10
+72.210.252.137
+74.82.47.194
+77.247.181.162
+77.247.181.163
+77.247.181.164
+77.247.181.165
+77.247.181.166
+77.250.227.202
+77.55.212.215
+77.68.42.132
+77.73.69.90
+77.81.104.124
+77.81.247.72
+78.109.23.2
+78.130.128.106
+78.142.175.70
+78.142.19.43
+78.21.17.242
+78.92.23.245
+79.134.234.247
+79.134.235.243
+79.134.235.253
+79.143.186.17
+79.172.193.32
+79.232.118.2
+80.127.116.96
+80.169.241.76
+80.241.60.207
+80.67.172.162
+80.68.92.225
+80.79.23.7
+81.169.136.206
+81.17.27.134
+81.17.27.135
+81.17.27.136
+81.17.27.137
+81.171.29.146
+81.49.51.12
+82.118.242.113
+82.118.242.128
+82.161.210.87
+82.221.128.191
+82.221.131.102
+82.221.131.5
+82.221.131.71
+82.221.139.190
+82.221.141.96
+82.223.14.245
+82.223.27.82
+82.228.252.20
+82.66.140.131
+82.94.132.34
+82.94.251.227
+83.136.106.136
+83.136.106.153
+84.19.182.33
+84.200.12.61
+84.200.50.18
+84.209.51.186
+84.53.192.243
+84.53.225.118
+85.119.82.142
+85.159.237.210
+85.214.243.115
+85.235.65.198
+85.248.227.163
+85.248.227.164
+85.248.227.165
+85.25.44.141
+86.104.15.15
+87.118.110.27
+87.118.112.63
+87.118.116.103
+87.118.116.12
+87.118.116.90
+87.118.122.30
+87.118.122.51
+87.118.92.43
+87.120.254.204
+87.120.254.223
+87.120.36.157
+87.122.229.240
+87.222.199.132
+87.64.102.248
+88.190.118.95
+88.77.181.199
+88.99.35.242
+89.14.189.217
+89.144.12.17
+89.187.143.31
+89.187.143.81
+89.203.249.251
+89.234.157.254
+89.234.190.157
+89.236.112.100
+89.31.57.58
+91.121.192.154
+91.121.251.65
+91.146.121.3
+91.153.76.138
+91.203.145.116
+91.203.146.126
+91.203.5.146
+91.203.5.165
+91.207.174.75
+91.219.236.171
+91.219.237.244
+91.219.238.95
+91.219.28.60
+91.221.57.179
+91.234.99.83
+91.250.241.241
+91.92.109.43
+91.92.109.53
+92.222.115.28
+92.222.180.10
+92.222.22.113
+92.222.38.67
+92.63.173.28
+93.115.241.194
+93.174.93.133
+93.174.93.6
+94.100.6.27
+94.100.6.72
+94.102.49.152
+94.102.51.78
+94.156.77.134
+94.230.208.147
+94.230.208.148
+94.242.57.161
+94.242.59.89
+94.32.66.15
+95.103.57.132
+95.128.43.164
+95.130.10.69
+95.130.11.170
+95.130.12.33
+95.130.9.90
+95.141.35.15
+95.142.161.63
+95.143.193.125
+95.165.133.22
+95.179.150.158
+95.211.118.194
+95.216.107.148
+95.216.145.1
+95.216.2.172
+95.42.126.41
+96.66.15.147
+96.70.31.155
+97.74.237.196
+98.174.90.43 \ No newline at end of file
diff --git a/cgi/weabot.py b/cgi/weabot.py
new file mode 100755
index 0000000..2e11252
--- /dev/null
+++ b/cgi/weabot.py
@@ -0,0 +1,1021 @@
+#!/usr/bin/python
+# coding=utf-8
+
+# Remove the first line to use the env command to locate python
+
+import os
+import time
+import datetime
+import random
+import cgi
+import _mysql
+from Cookie import SimpleCookie
+
+import tenjin
+import manage
+import oekaki
+import gettext
+from database import *
+from settings import Settings
+from framework import *
+from formatting import *
+from post import *
+from img import *
+
+__version__ = "0.8.7"
+
+# Set to True to disable weabot's exception routing and enable profiling
+_DEBUG = False
+
+# Set to True to save performance data to weabot.txt
+_LOG = False
+
+class weabot(object):
+ def __init__(self, environ, start_response):
+ global _DEBUG
+ self.environ = environ
+ if self.environ["PATH_INFO"].startswith("/weabot.py/"):
+ self.environ["PATH_INFO"] = self.environ["PATH_INFO"][11:]
+
+ self.start = start_response
+ self.formdata = getFormData(self)
+
+ self.output = ""
+
+ self.handleRequest()
+
+ # Localization Code
+ lang = gettext.translation('weabot', './locale', languages=[Settings.LANG])
+ lang.install()
+
+ logTime("**Start**")
+ if _DEBUG:
+ import cProfile
+
+ prof = cProfile.Profile()
+ prof.runcall(self.run)
+ prof.dump_stats('stats.prof')
+ else:
+ try:
+ self.run()
+ except UserError, message:
+ self.error(message)
+ except Exception, inst:
+ import sys, traceback
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ detail = ((os.path.basename(o[0]),o[1],o[2],o[3]) for o in traceback.extract_tb(exc_traceback))
+ self.exception(type(inst), inst, detail)
+
+ # close database and finish
+ CloseDb()
+ logTime("**End**")
+
+ if _LOG:
+ logfile = open(Settings.ROOT_DIR + "weabot.txt", "w")
+ logfile.write(logTimes())
+ logfile.close()
+
+ def __iter__(self):
+ self.handleResponse()
+ self.start("200 OK", self.headers)
+ yield self.output
+
+ def error(self, message):
+ board = Settings._.BOARD
+ if board:
+ if board['board_type'] == '1':
+ info = {}
+ info['host'] = self.environ["REMOTE_ADDR"]
+ info['name'] = self.formdata.get('fielda', '')
+ info['email'] = self.formdata.get('fieldb', '')
+ info['message'] = self.formdata.get('message', '')
+
+ self.output += renderTemplate("txt_error.html", {"info": info, "error": message})
+ else:
+ mobile = self.formdata.get('mobile', '')
+ if mobile:
+ self.output += renderTemplate("mobile/error.html", {"error": message})
+ else:
+ self.output += renderTemplate("error.html", {"error": message, "boards_url": Settings.BOARDS_URL, "board": board["dir"]})
+ else:
+ self.output += renderTemplate("exception.html", {"exception": None, "error": message})
+
+ def exception(self, type, message, detail):
+ self.output += renderTemplate("exception.html", {"exception": type, "error": message, "detail": detail})
+
+ def handleRequest(self):
+ self.headers = [("Content-Type", "text/html")]
+ self.handleCookies()
+
+ def handleResponse(self):
+ if self._cookies is not None:
+ for cookie in self._cookies.values():
+ self.headers.append(("Set-Cookie", cookie.output(header="")))
+
+ def handleCookies(self):
+ self._cookies = SimpleCookie()
+ self._cookies.load(self.environ.get("HTTP_COOKIE", ""))
+
+ def run(self):
+ path_split = self.environ["PATH_INFO"].split("/")
+ caught = False
+
+ if Settings.FULL_MAINTENANCE:
+ raise UserError, _("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE
+
+ if len(path_split) > 1:
+ if path_split[1] == "post":
+ # Making a post
+ caught = True
+
+ if 'password' not in self.formdata:
+ raise UserError, "El request está incompleto."
+
+ # let's get all the POST data we need
+ ip = self.environ["REMOTE_ADDR"]
+ boarddir = self.formdata.get('board')
+ parent = self.formdata.get('parent')
+ trap1 = self.formdata.get('name', '')
+ trap2 = self.formdata.get('email', '')
+ name = self.formdata.get('fielda', '')
+ email = self.formdata.get('fieldb', '')
+ subject = self.formdata.get('subject', '')
+ message = self.formdata.get('message', '')
+ file = self.formdata.get('file')
+ file_original = self.formdata.get('file_original')
+ spoil = self.formdata.get('spoil')
+ oek_file = self.formdata.get('oek_file')
+ password = self.formdata.get('password', '')
+ noimage = self.formdata.get('noimage')
+ mobile = ("mobile" in self.formdata.keys())
+
+ # call post function
+ (post_url, ttaken) = self.make_post(ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile)
+
+ # make redirect
+ self.output += make_redirect(post_url, ttaken)
+ elif path_split[1] == "environ":
+ caught = True
+
+ self.output += repr(self.environ)
+ elif path_split[1] == "delete":
+ # Deleting a post
+ caught = True
+
+ boarddir = self.formdata.get('board')
+ postid = self.formdata.get('delete')
+ imageonly = self.formdata.get('imageonly')
+ password = self.formdata.get('password')
+ mobile = self.formdata.get('mobile')
+
+ # call delete function
+ self.delete_post(boarddir, postid, imageonly, password, mobile)
+ elif path_split[1] == "anarkia":
+ import anarkia
+ caught = True
+ OpenDb()
+ anarkia.anarkia(self, path_split)
+ elif path_split[1] == "manage":
+ caught = True
+ OpenDb()
+ manage.manage(self, path_split)
+ elif path_split[1] == "api":
+ import api
+ caught = True
+ self.headers = [("Content-Type", "application/json")]
+ OpenDb()
+ api.api(self, path_split)
+ elif path_split[1] == "threadlist":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ if board['board_type'] != '1':
+ raise UserError, "No disponible para esta sección."
+ self.output = threadList(0)
+ elif path_split[1] == "mobile":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ self.output = threadList(1)
+ elif path_split[1] == "mobilelist":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ self.output = threadList(2)
+ elif path_split[1] == "mobilecat":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ self.output = threadList(3)
+ elif path_split[1] == "mobilenew":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ self.output = renderTemplate('txt_newthread.html', {}, True)
+ elif path_split[1] == "mobilehome":
+ OpenDb()
+ latest_age = getLastAge(Settings.HOME_LASTPOSTS)
+ for threads in latest_age:
+ content = threads['url']
+ content = content.replace('/read/', '/')
+ content = content.replace('/res/', '/')
+ content = content.replace('.html', '')
+ threads['url'] = content
+ caught = True
+ self.output = renderTemplate('latest.html', {'latest_age': latest_age}, True)
+ elif path_split[1] == "mobilenewest":
+ OpenDb()
+ newthreads = getNewThreads(Settings.HOME_LASTPOSTS)
+ for threads in newthreads:
+ content = threads['url']
+ content = content.replace('/read/', '/')
+ content = content.replace('/res/', '/')
+ content = content.replace('.html', '')
+ threads['url'] = content
+ caught = True
+ self.output = renderTemplate('newest.html', {'newthreads': newthreads}, True)
+ elif path_split[1] == "mobileread":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ if len(path_split) > 4 and path_split[4] and board['board_type'] == '1':
+ #try:
+ self.output = dynamicRead(int(path_split[3]), path_split[4], True)
+ #except:
+ # self.output = threadPage(path_split[3], True)
+ elif board['board_type'] == '1':
+ self.output = threadPage(0, True, path_split[3])
+ else:
+ self.output = threadPage(path_split[3], True)
+ elif path_split[1] == "catalog":
+ OpenDb()
+ board = setBoard(path_split[2])
+ caught = True
+ sort = self.formdata.get('sort', '')
+ self.output = catalog(sort)
+ elif path_split[1] == "oekaki":
+ caught = True
+ OpenDb()
+ oekaki.oekaki(self, path_split)
+ elif path_split[1] == "play":
+ # Module player
+ caught = True
+ boarddir = path_split[2]
+ modfile = path_split[3]
+ self.output = renderTemplate('mod.html', {'board': boarddir, 'modfile': modfile})
+ elif path_split[1] == "report":
+ # Report post, check if they are enabled
+ # Can't report if banned
+ caught = True
+ ip = self.environ["REMOTE_ADDR"]
+ boarddir = path_split[2]
+ postid = int(path_split[3])
+ reason = self.formdata.get('reason')
+ try:
+ txt = True
+ postshow = int(path_split[4])
+ except:
+ txt = False
+ postshow = postid
+
+ self.report(ip, boarddir, postid, reason, txt, postshow)
+ elif path_split[1] == "stats":
+ caught = True
+ self.stats()
+ elif path_split[1] == "random":
+ caught = True
+ OpenDb()
+ board = FetchOne("SELECT `id`, `dir`, `board_type` FROM `boards` WHERE `secret` = 0 AND `id` <> 1 AND `id` <> 13 AND `id` <> 34 ORDER BY RAND() LIMIT 1")
+ thread = FetchOne("SELECT `id`, `timestamp` FROM `posts` WHERE `parentid` = 0 AND `boardid` = %s ORDER BY RAND() LIMIT 1" % board['id'])
+ if board['board_type'] == '1':
+ url = Settings.HOME_URL + board['dir'] + '/read/' + thread['timestamp'] + '/'
+ else:
+ url = Settings.HOME_URL + board['dir'] + '/res/' + thread['id'] + '.html'
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><meta http-equiv="refresh" content="0;url=%s" /><body><p>...</p></body></html>' % url
+ elif path_split[1] == "nostalgia":
+ caught = True
+ OpenDb()
+ thread = FetchOne("SELECT `timestamp` FROM `archive` WHERE `boardid` = 9 AND `timestamp` < 1462937230 ORDER BY RAND() LIMIT 1")
+ url = Settings.HOME_URL + '/zonavip/read/' + thread['timestamp'] + '/'
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><meta http-equiv="refresh" content="0;url=%s" /><body><p>...</p></body></html>' % url
+ elif path_split[1] == "banned":
+ OpenDb()
+ packed_ip = inet_aton(self.environ["REMOTE_ADDR"])
+ bans = FetchAll("SELECT * FROM `bans` WHERE (`netmask` IS NULL AND `ip` = '"+str(packed_ip)+"') OR (`netmask` IS NOT NULL AND '"+str(packed_ip)+"' & `netmask` = `ip`)")
+ if bans:
+ for ban in bans:
+ if ban["boards"] != "":
+ boards = pickle.loads(ban["boards"])
+ if ban["boards"] == "" or path_split[2] in boards:
+ caught = True
+ if ban["boards"]:
+ boards_str = '/' + '/, /'.join(boards) + '/'
+ else:
+ boards_str = _("all boards")
+ if ban["until"] != "0":
+ expire = formatTimestamp(ban["until"])
+ else:
+ expire = ""
+
+ template_values = {
+ 'cgi_url': Settings.CGI_URL,
+ 'return_board': path_split[2],
+ 'boards_str': boards_str,
+ 'reason': ban['reason'],
+ 'added': formatTimestamp(ban["added"]),
+ 'expire': expire,
+ 'ip': self.environ["REMOTE_ADDR"],
+ }
+ self.output = renderTemplate('banned.html', template_values)
+ else:
+ if len(path_split) > 2:
+ caught = True
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>%s</p></body></html>' % (Settings.HOME_URL + path_split[2], _("Your ban has expired. Redirecting..."))
+ elif path_split[1] == "read":
+ # Textboard read:
+ if len(path_split) > 4:
+ caught = True
+ # 2: board
+ # 3: thread
+ # 4: post(s)
+ OpenDb()
+ board = setBoard(path_split[2])
+ self.output = dynamicRead(int(path_split[3]), path_split[4])
+ elif path_split[1] == "preview":
+ caught = True
+ OpenDb()
+ try:
+ board = setBoard(self.formdata["board"])
+ message = format_post(self.formdata["message"], self.environ["REMOTE_ADDR"], self.formdata["parentid"])
+ self.output = message
+ except Exception, messagez:
+ self.output = "Error: " + str(messagez) + " : " + str(self.formdata)
+ if not caught:
+ # Redirect the user back to the front page
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>--&gt; --&gt; --&gt;</p></body></html>' % Settings.HOME_URL
+
+ def make_post(self, ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile):
+ _STARTTIME = time.clock() # Comment if not debug
+
+ # open database
+ OpenDb()
+
+ # set the board
+ board = setBoard(boarddir)
+
+ if board["dir"] != ["anarkia"]:
+ if addressIsProxy(ip):
+ raise UserError, "Proxy prohibido en esta sección."
+
+ # check length of fields
+ if len(name) > 50:
+ raise UserError, "El campo de nombre es muy largo."
+ if len(email) > 50:
+ raise UserError, "El campo de e-mail es muy largo."
+ if len(subject) > 100:
+ raise UserError, "El campo de asunto es muy largo."
+ if len(message) > 8000:
+ raise UserError, "El campo de mensaje es muy largo."
+ if message.count('\n') > 50:
+ raise UserError, "El mensaje tiene muchos saltos de línea."
+
+ # anti-spam trap
+ if trap1 or trap2:
+ raise UserError, "Te quedan tres días de vida."
+
+ # Create a single datetime now so everything syncs up
+ t = time.time()
+
+ # Delete expired bans
+ deletedBans = UpdateDb("DELETE FROM `bans` WHERE `until` != 0 AND `until` < " + str(timestamp()))
+ if deletedBans > 0:
+ regenerateAccess()
+
+ # Redirect to ban page if user is banned
+ if addressIsBanned(ip, board["dir"]):
+ #raise UserError, 'Tu host está en la lista negra.'
+ raise UserError, '<meta http-equiv="refresh" content="0; url=/cgi/banned/%s">' % board["dir"]
+
+ # Disallow posting if the site OR board is in maintenance
+ if Settings.MAINTENANCE:
+ raise UserError, _("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE
+ if board["locked"] == '1':
+ raise UserError, _("This board is closed. You can't post in it.")
+
+ # create post object
+ post = Post(board["id"])
+ post["ip"] = inet_aton(ip)
+ post["timestamp"] = post["bumped"] = int(t)
+ post["timestamp_formatted"] = formatTimestamp(t)
+
+ # load parent info if we are replying
+ parent_post = None
+ parent_timestamp = post["timestamp"]
+ if parent:
+ parent_post = get_parent_post(parent, board["id"])
+ parent_timestamp = parent_post['timestamp']
+ post["parentid"] = parent_post['id']
+ post["bumped"] = parent_post['bumped']
+ if parent_post['locked'] == '1':
+ raise UserError, _("The thread is closed. You can't post in it.")
+
+ # check if the user is flooding
+ flood_check(t, post, board["id"])
+
+ # use fields only if enabled
+ if board["disable_name"] != '1':
+ post["name"] = cleanString(name)
+ post["email"] = cleanString(email, quote=True)
+ if board["disable_subject"] != '1':
+ post["subject"] = cleanString(subject)
+
+ # process tripcodes
+ post["name"], post["tripcode"] = tripcode(post["name"])
+
+ # Remove carriage return, they're useless
+ message = message.replace("\r", "")
+
+ # check ! functions before
+ extend = extend_str = dice = ball = None
+
+ if not post["parentid"] and board["dir"] not in ['bai', 'world']:
+ # creating thread
+ __extend = re.compile(r"^!extend(:\w+)(:\w+)?\n")
+ res = __extend.match(message)
+ if res:
+ extend = res.groups()
+ # truncate extend
+ extend_str = res.group(0)
+ message = message[res.end(0):]
+
+ if board["dir"] in ['juegos', '0', 'polka']:
+ __dice = re.compile(r"^!dado(:\w+)(:\w+)?\n")
+ res = __dice.match(message)
+ if res:
+ dice = res.groups()
+ message = message[res.end(0):]
+
+ if board["dir"] in ['zonavip', '0', 'polka']:
+ __ball = re.compile(r"^!bola8\n")
+ res = __ball.match(message)
+ if res:
+ ball = True
+ message = message[res.end(0):]
+
+ # use and format message
+ if message.strip():
+ post["message"] = format_post(message, ip, post["parentid"], parent_timestamp)
+
+ # add function messages
+ if extend_str:
+ extend_str = extend_str.replace('!extend', 'EXTEND')
+ post["message"] += '<hr />' + extend_str + ' configurado.'
+ if dice:
+ post["message"] += '<hr />' + throw_dice(dice)
+ if ball:
+ post["message"] += '<hr />' + magic_ball()
+
+ # remove sage from wrong fields
+ if post["name"].lower() == 'sage':
+ post["name"] = random.choice(board["anonymous"].split('|'))
+ if post["subject"].lower() == 'sage':
+ post["subject"] = board["subject"]
+
+ if not post["parentid"] and post["email"].lower() == 'sage':
+ post["email"] = ""
+
+ # disallow illegal characters
+ if post["name"]:
+ post["name"] = post["name"].replace('★', '☆')
+ post["name"] = post["name"].replace('â—†', 'â—‡')
+
+ # process capcodes
+ cap_id = hide_end = None
+ if post["name"] in Settings.CAPCODES:
+ capcode = Settings.CAPCODES[post["name"]]
+ if post["tripcode"] == (Settings.TRIP_CHAR + capcode[0]):
+ post["name"], post["tripcode"] = capcode[1], capcode[2]
+ #if board['board_type'] == '1':
+ # post["name"], post["tripcode"] = capcode[1], ''
+ #else:
+ # post["name"] = post["tripcode"] = ''
+ # post["message"] = ('[<span style="color:red">%s</span>]<br />' % capcode[2]) + post["message"]
+
+ cap_id, hide_end = capcode[3], capcode[4]
+
+ # hide ip if necessary
+ if hide_end:
+ post["ip"] = 0
+
+ # use password
+ post["password"] = password
+
+ # EXTEND feature
+ if post["parentid"] and board["dir"] not in ['bai', 'world']:
+ # replying
+ __extend = re.compile(r"<hr />EXTEND(:\w+)(:\w+)?\b")
+ res = __extend.search(parent_post["message"])
+ if res:
+ extend = res.groups()
+
+ # compatibility : old id function
+ if 'id' in parent_post["email"]:
+ board["useid"] = '3'
+
+ if 'id' in post["email"]:
+ board["useid"] = '3'
+
+ if extend:
+ try:
+ # 1: ID
+ if extend[0] == ':no':
+ board["useid"] = '0'
+ elif extend[0] == ':yes':
+ board["useid"] = '1'
+ elif extend[0] == ':force':
+ board["useid"] = '2'
+ elif extend[0] == ':extra':
+ board["useid"] = '3'
+
+ # 2: Slip
+ if extend[1] == ':no':
+ board["slip"] = '0'
+ elif extend[1] == ':yes':
+ board["slip"] = '1'
+ elif extend[1] == ':domain':
+ board["slip"] = '2'
+ elif extend[1] == ':verbose':
+ board["slip"] = '3'
+ elif extend[1] == ':country':
+ board["countrycode"] = '1'
+ elif extend[1] == ':all':
+ board["slip"] = '3'
+ board["countrycode"] = '1'
+ except IndexError:
+ pass
+
+ # if we are replying, use first post's time
+ if post["parentid"]:
+ tim = parent_post["timestamp"]
+ else:
+ tim = post["timestamp"]
+
+ # make ID hash
+ if board["useid"] != '0':
+ post["timestamp_formatted"] += ' ID:' + iphash(ip, post, tim, board["useid"], mobile, self.environ["HTTP_USER_AGENT"], cap_id, hide_end, (board["countrycode"] in ['1', '2']))
+
+ # use for future file checks
+ xfile = (file or oek_file)
+
+ # textboard inforcements (change it to settings maybe?)
+ if board['board_type'] == '1':
+ if not post["parentid"] and not post["subject"]:
+ raise UserError, _("You must enter a title to create a thread.")
+ if not post["message"]:
+ raise UserError, _("Please enter a message.")
+ else:
+ if not post["parentid"] and not xfile and not noimage:
+ raise UserError, _("You must upload an image first to create a thread.")
+ if not xfile and not post["message"]:
+ raise UserError, _("Please enter a message or upload an image to reply.")
+
+ # check if this post is allowed
+ if post["parentid"]:
+ if file and board['allow_image_replies'] == '0':
+ raise UserError, _("Image replies not allowed.")
+ else:
+ if file and board['allow_images'] == '0':
+ raise UserError, _("No images allowed.")
+
+ # use default values when missing
+ if not post["name"] and not post["tripcode"]:
+ post["name"] = random.choice(board["anonymous"].split('|'))
+ if not post["subject"] and not post["parentid"]:
+ post["subject"] = board["subject"]
+ if not post["message"]:
+ post["message"] = board["message"]
+
+ # process files
+ if oek_file:
+ try:
+ fname = "%s/oek_temp/%s.png" % (Settings.HOME_DIR, oek_file)
+ with open(fname) as f:
+ file = f.read()
+ os.remove(fname)
+ except:
+ raise UserError, "Imposible leer la imagen oekaki."
+
+ if file and not noimage:
+ post = processImage(post, file, t, file_original, (spoil and board['allow_spoilers'] == '1'))
+
+ # slip
+ if board["slip"] != '0':
+ slips = []
+
+ # name
+ if board["slip"] in ['1', '3']:
+ if time.strftime("%H") in ['00', '24'] and time.strftime("%M") == '00' and time.strftime("%S") == '00':
+ host_nick = '000000'
+ else:
+ host_nick = 'sarin'
+
+ if hide_end:
+ host_nick = '★'
+ elif addressIsTor(ip):
+ host_nick = 'onion'
+ else:
+ isps = {'cablevision': 'easy',
+ 'cantv': 'warrior',
+ 'claro': 'america',
+ 'cnet': 'nova',
+ 'copelnet': 'cisneros',
+ 'cps.com': 'silver',
+ 'cybercable': 'bricklayer',
+ 'entel': 'matte',
+ 'eternet': 'stream',
+ 'fibertel': 'roughage',
+ 'geonet': 'thunder',
+ 'gtdinternet': 'casanueva',
+ 'ifxnw': 'effect',
+ 'infinitum': 'telegraph',
+ 'intercable': 'easy',
+ 'intercity': 'cordoba',
+ 'iplannet': 'conquest',
+ 'itcsa.net': 'sarmiento',
+ 'megared': 'clear',
+ 'movistar': 'bell',
+ 'nextel': 'fleet',
+ 'speedy': 'oxygen',
+ 'telecom': 'license',
+ 'telmex': 'slender',
+ 'telnor': 'compass',
+ 'tie.cl': 'bell',
+ 'vtr.net': 'liberty',
+ 'utfsm': 'virgin',
+ }
+ host = getHost(ip)
+
+ if host:
+ for k, v in isps.iteritems():
+ if k in host:
+ host_nick = v
+ break
+
+ slips.append(host_nick)
+
+ # hash
+ if board["slip"] in ['1', '3']:
+ if hide_end:
+ slips.append('-'.join(('****', '****')))
+ elif addressIsTor(ip):
+ slips.append('-'.join(('****', getMD5(os.environ["HTTP_USER_AGENT"])[:4])))
+ else:
+ slips.append('-'.join((getMD5(ip)[:4], getMD5(os.environ["HTTP_USER_AGENT"])[:4])))
+
+ # host
+ if board["slip"] == '2':
+ if hide_end:
+ host = '★'
+ elif addressIsTor(ip):
+ host = 'onion'
+ else:
+ host = getHost(ip)
+ if host:
+ hosts = host.split('.')
+ if len(hosts) > 2:
+ if hosts[-2] in ['ne', 'net', 'com', 'co']:
+ host = '.'.join((hosts[-3], hosts[-2], hosts[-1]))
+ else:
+ host = '.'.join((hosts[-2], hosts[-1]))
+ host = '*.' + host
+ else:
+ iprs = ip.split('.')
+ host = '%s.%s.*.*' % (iprs[0], iprs[1])
+ slips.append(host)
+
+ # IP
+ if board["slip"] == '3':
+ if hide_end:
+ host = '[*.*.*.*]'
+ else:
+ iprs = ip.split('.')
+ host = '[%s.%s.*.*]' % (iprs[0], iprs[1])
+ slips.append(host)
+
+ if slips:
+ post["tripcode"] += " (%s)" % ' '.join(slips)
+
+ # country code
+ if board["countrycode"] == '1':
+ if hide_end or addressIsTor(ip):
+ country = '??'
+ else:
+ country = getCountry(ip)
+ post["name"] += " <em>[%s]</em>" % country
+
+ # set expiration date if necessary
+ if board["maxage"] != '0' and not post["parentid"]:
+ if board["dir"] == '2d':
+ date_format = '%m月%d日'
+ date_format_y = '%Y年%m月'
+ else:
+ date_format = '%d/%m'
+ date_format_y = '%m/%Y'
+ post["expires"] = int(t) + (int(board["maxage"]) * 86400)
+ if int(board["maxage"]) >= 365:
+ date_format = date_format_y
+ post["expires_formatted"] = datetime.datetime.fromtimestamp(post["expires"]).strftime(date_format)
+
+ if not post["parentid"]:
+ # fill with default values if creating a new thread
+ post["length"] = 1
+ post["last"] = post["timestamp"]
+
+ if board["dir"] == 'noticias':
+ # check if there's at least one link
+ if "<a href" not in post["message"]:
+ raise UserError, "Al momento de crear un hilo en esta sección necesitas incluír al menos 1 link como fuente en tu mensaje."
+
+ # insert icon if needed
+ img_src = '<img src="%s" alt="ico" /><br />' % getRandomIco()
+ post["message"] = img_src + post["message"]
+
+ # insert post, then run timThreads to make sure the board doesn't exceed the page limit
+ postid = post.insert()
+
+ # delete threads that have crossed last page
+ trimThreads()
+
+ # fix null references when creating thread
+ if board["board_type"] == '1' and not post["parentid"]:
+ post["message"] = re.compile(r'<a href="/(\w+)/res/0.html/(.+)"').sub(r'<a href="/\1/res/'+str(postid)+r'.html/\2"', post["message"])
+ UpdateDb("UPDATE `posts` SET message = '%s' WHERE boardid = '%s' AND id = '%s'" % (_mysql.escape_string(post["message"]), _mysql.escape_string(board["id"]), _mysql.escape_string(str(postid))))
+
+ # do operations if replying to a thread (bump, autoclose, update cache)
+ logTime("Updating thread")
+ thread_length = None
+ if post["parentid"]:
+ # get length of the thread
+ thread_length = threadNumReplies(post["parentid"])
+
+ # bump if not saged
+ if 'sage' not in post["email"].lower() and parent_post['locked'] != '2':
+ UpdateDb("UPDATE `posts` SET bumped = %d WHERE (`id` = '%s' OR `parentid` = '%s') AND `boardid` = '%s'" % (post["timestamp"], post["parentid"], post["parentid"], board["id"]))
+
+ # check if thread must be closed
+ autoclose_thread(post["parentid"], t, thread_length)
+
+ # update final attributes (length and last post)
+ UpdateDb("UPDATE `posts` SET length = %d, last = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (thread_length, post["timestamp"], post["parentid"], board["id"]))
+
+ # update cache
+ threadUpdated(post["parentid"])
+ else:
+ # create cache for new thread
+ threadUpdated(postid)
+
+ regenerateHome()
+
+ # make page redirect
+ ttaken = timeTaken(_STARTTIME, time.clock())
+ noko = 'noko' in email.lower() or (board["board_type"] == '1')
+
+ # get new post url
+ post_url = make_url(postid, post, parent_post or post, noko, mobile)
+
+ if board['secret'] == '0':
+ # add to recent posts
+ if Settings.ENABLE_RSS:
+ latestAdd(post, thread_length, postid, parent_post)
+ # call discord hook
+ if Settings.ENABLE_DISCORD_HOOK and not post["parentid"]:
+ hook_url = make_url(postid, post, parent_post or post, True, False)
+ discord_hook(post, hook_url)
+
+ return (post_url, ttaken)
+
+ def delete_post(self, boarddir, postid, imageonly, password, mobile=False):
+ # open database
+ OpenDb()
+
+ # set the board
+ board = setBoard(boarddir)
+
+ if board["dir"] == '0':
+ raise UserError, "No se pueden eliminar mensajes en esta sección."
+
+ # check if we have a post id and check it's numeric
+ if not postid:
+ raise UserError, "Selecciona uno o más mensajes a eliminar."
+
+ # make sure we have a password
+ if not password:
+ raise UserError, _("Please enter a password.")
+
+ to_delete = []
+ if isinstance(postid, list):
+ to_delete = [n.value for n in postid]
+ else:
+ to_delete = [postid]
+
+ # delete posts
+ if board['board_type'] == '1' and len(to_delete) == 1:
+ # we should be deleting only one (textboard)
+ # check if it's the last post and delete it completely if it is
+ deltype = '0'
+ post = FetchOne("SELECT `id`, `timestamp`, `parentid` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(to_delete[0])))
+ if post['parentid'] != '0':
+ op = get_parent_post(post['parentid'], board['id'])
+ if op['last'] != post['timestamp']:
+ deltype = '1'
+
+ deletePost(to_delete[0], password, deltype, imageonly)
+ latestRemove(post['id'])
+ regenerateHome()
+ else:
+ # delete all checked posts (IB)
+ deleted = 0
+ errors = 0
+ msgs = []
+
+ for pid in to_delete:
+ try:
+ deletePost(pid, password, board['recyclebin'], imageonly)
+ latestRemove(pid)
+ deleted += 1
+ msgs.append('No.%s: Eliminado' % pid)
+ except UserError, message:
+ errors += 1
+ msgs.append('No.%s: %s' % (pid, message))
+
+ # regenerate home
+ if deleted:
+ regenerateHome()
+
+ # show errors, if any
+ if errors:
+ raise UserError, 'No todos los mensajes pudieron ser eliminados.<br />' + '<br />'.join(msgs)
+
+ # redirect
+ if imageonly:
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s/" /><p>%s</p></body></html>' % (("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + board["dir"], _("File deleted successfully."))
+ else:
+ self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s/" /><p>%s</p></body></html>' % (("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + board["dir"], _("Post deleted successfully."))
+
+ def report(self, ip, boarddir, postid, reason, txt, postshow):
+ # don't allow if the report system is off
+ if not Settings.REPORTS_ENABLE:
+ raise UserError, _('Report system is deactivated.')
+
+ # if there's not a reason, show the report page
+ if reason is None:
+ self.output += renderTemplate("report.html", {'finished': False, 'postshow': postshow, 'txt': txt})
+ return
+
+ # check reason
+ if not reason:
+ raise UserError, _("Enter a reason.")
+ if len(reason) > 100:
+ raise UserError, _("Text too long.")
+
+ # open database
+ OpenDb()
+
+ # set the board we're in
+ board = setBoard(boarddir)
+
+ # check if he's banned
+ if addressIsBanned(ip, board["dir"]):
+ raise UserError, _("You're banned.")
+
+ # check if post exists
+ post = FetchOne("SELECT `id`, `parentid`, `ip` FROM `posts` WHERE `id` = '%s' AND `boardid` = '%s'" % (_mysql.escape_string(str(postid)), _mysql.escape_string(board['id'])))
+ if not post:
+ raise UserError, _("Post doesn't exist.")
+
+ # generate link
+ if board["board_type"] == '1':
+ parent_post = get_parent_post(post["parentid"], board["id"])
+ link = "/%s/read/%s/%s" % (board["dir"], parent_post["timestamp"], postshow)
+ else:
+ link = "/%s/res/%s.html#%s" % (board["dir"], post["parentid"], post["id"])
+
+ # insert report
+ t = time.time()
+ message = cgi.escape(self.formdata["reason"]).strip()[0:8000]
+ message = message.replace("\n", "<br />")
+
+ UpdateDb("INSERT INTO `reports` (board, postid, parentid, link, ip, reason, reporterip, timestamp, timestamp_formatted) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (board["dir"], post['id'], post['parentid'], link, post['ip'], _mysql.escape_string(message), _mysql.escape_string(self.environ["REMOTE_ADDR"]), str(t), formatTimestamp(t)))
+ self.output = renderTemplate("report.html", {'finished': True})
+
+ def stats(self):
+ import json, math, platform
+ try:
+ with open('stats.json', 'r') as f:
+ out = json.load(f)
+ except ValueError:
+ out = {'t': 0}
+
+ regenerated = False
+ if (time.time() - out['t']) > 3600:
+ regenerated = True
+
+ # open database
+ OpenDb()
+
+ # 1 week = 604800
+ query_day = FetchAll("SELECT DATE_FORMAT(FROM_UNIXTIME(FLOOR((timestamp-10800)/86400)*86400+86400), \"%Y-%m-%d\"), COUNT(1), COUNT(IF(parentid=0, 1, NULL)) "
+ "FROM posts "
+ "WHERE (timestamp-10800) > (UNIX_TIMESTAMP()-604800) AND (IS_DELETED = 0 OR IS_DELETED = 3) "
+ "GROUP BY FLOOR((timestamp-10800)/86400) "
+ "ORDER BY FLOOR((timestamp-10800)/86400)", 0)
+
+ query_count = FetchOne("SELECT COUNT(1), COUNT(NULLIF(file, '')), VERSION() FROM posts", 0)
+ total = int(query_count[0])
+ total_files = int(query_count[1])
+ mysql_ver = query_count[2]
+
+ archive_count = FetchOne("SELECT SUM(length) FROM archive", 0)
+ total_archived = int(archive_count[0])
+
+ days = []
+ for date, count, threads in query_day[1:]:
+ days.append( (date, count, threads) )
+
+ query_b = FetchAll("SELECT id, dir, name FROM boards WHERE boards.secret = 0", 0)
+
+ boards = []
+ totalp = 0
+ for id, dir, longname in query_b:
+ bposts = FetchOne("SELECT COUNT(1) FROM posts "
+ "WHERE '"+str(id)+"' = posts.boardid AND timestamp > ( UNIX_TIMESTAMP(DATE(NOW())) - 2419200 )", 0)
+ boards.append( (dir, longname, int(bposts[0])) )
+ totalp += int(bposts[0])
+
+ boards = sorted(boards, key=lambda boards: boards[2], reverse=True)
+
+ boards_percent = []
+ for dir, longname, bposts in boards:
+ if bposts > 0:
+ boards_percent.append( (dir, longname, '{0:.2f}'.format( float(bposts)*100/totalp ), int(bposts) ) )
+ else:
+ boards_percent.append( (dir, longname, '0.00', '0' ) )
+
+ #posts = FetchAll("SELECT `parentid`, `boardid` FROM `posts` INNER JOIN `boards` ON posts.boardid = boards.id WHERE posts.parentid<>0 AND posts.timestamp>(UNIX_TIMESTAMP()-86400) AND boards.secret=0 ORDER BY `parentid`")
+ #threads = {}
+ #for post in posts:
+ # if post["parentid"] in threads:
+ # threads[post["parentid"]] += 1
+ # else:
+ # threads[post["parentid"]] = 1
+
+ python_version = platform.python_version()
+ if self.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'):
+ python_version += " (CGI)"
+ else:
+ python_version += " (FastCGI)"
+
+ out = {
+ "uname": platform.uname(),
+ "python_ver": python_version,
+ "python_impl": platform.python_implementation(),
+ "python_build": platform.python_build()[1],
+ "python_compiler": platform.python_compiler(),
+ "mysql_ver": mysql_ver,
+ "tenjin_ver": tenjin.__version__,
+ "weabot_ver": __version__,
+ "days": days,
+ "boards": boards,
+ "boards_percent": boards_percent,
+ "total": total,
+ "total_files": total_files,
+ "total_archived": total_archived,
+ "t": timestamp(),
+ "tz": Settings.TIME_ZONE,
+ }
+ with open('stats.json', 'w') as f:
+ json.dump(out, f)
+
+ out['timestamp'] = re.sub(r"\(...\)", " ", formatTimestamp(out['t']))
+ out['regenerated'] = regenerated
+ self.output = renderTemplate("stats.html", out)
+ #self.headers = [("Content-Type", "application/json")]
+
+if __name__ == "__main__":
+ from fcgi import WSGIServer
+
+ # Psyco is not required, however it will be used if available
+ try:
+ import psyco
+ logTime("Psyco se ha instalado")
+ psyco.bind(tenjin.helpers.to_str)
+ psyco.bind(weabot.run, 2)
+ psyco.bind(getFormData)
+ psyco.bind(setCookie)
+ psyco.bind(threadUpdated)
+ psyco.bind(processImage)
+ except:
+ pass
+
+ WSGIServer(weabot).run()
+
diff --git a/static/css/buri.css b/static/css/buri.css
new file mode 100644
index 0000000..b8c1eb5
--- /dev/null
+++ b/static/css/buri.css
@@ -0,0 +1,23 @@
+html,body{background:#EEF2FF;color:#000;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+a,a .name,a .name b,.nav label:hover{color:#34345C}
+a.rep{color:#000;text-decoration:underline}
+a:visited{color:#34345C}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover{color:#DD0000}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#0010E0}
+.extramode{background:#0040E0}
+.postblock{background:#9988EE;color:#000}
+.q{color:#789922}
+.fs{text-decoration:none}
+.subj{color:#0F0C5D;font-weight:bold}
+.name{color:#228854}
+.name b{color:#117743}
+.reply,#q-p{background:#D6DAF0;color:#000}
+.omitted,.abbrev{color:#707070}
+.highlight{background:#c1c6e2}
+.managertable td{background:#9AD2F6;color:#000}
+.managertable th{background:#0F8FE1;color:#000}
+#catalog .thread:hover{background:#D6DAF0;box-shadow:0 0 5px 5px #D6DAF0}
+#catalog .replies{color:#555555;font-weight:bold}
+.yt{background:#E3E6F5;border:1px solid #C1C6E7}
+.quoted{border-color:#EEF2FF;color:#707070} \ No newline at end of file
diff --git a/static/css/cyber.css b/static/css/cyber.css
new file mode 100644
index 0000000..e9732bc
--- /dev/null
+++ b/static/css/cyber.css
@@ -0,0 +1,39 @@
+html,body{background:#000 url('img/cyb.png');color:#61CE3C;font-family:monospace}
+a,a .name,a .name b,.nav label{color:#C80B63}
+.reflink a,.rep,.hsbn,#main_nav a{color:#FFF}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover,#main_nav a:hover{color:#F00}
+.reflink a:hover{text-decoration:underline}
+hr{display:none}
+.thread hr{border:0;border-top:1px solid #61CE3C;display:block}
+input,input[type="text"],input[type="password"],textarea{background:#1A1A1A;border:0;color:#61CE3C}
+input[type="submit"]{border:1px outset #999}
+input[type="submit"]:active{border-style:inset}
+#main_nav{background:#1A1A1A;border-bottom:double 3px #989898;color:#989898;margin:-10px -10px 0;padding:10px}
+.logo{color:#C00}
+.replymode,.extramode{color:#FFF;text-shadow:1px 1px #000}
+.replymode{background:#C00;box-shadow:5px 5px #989898;margin-bottom:1em}
+.extramode{background:#001eff;}
+.postform,.oekform{background:#2b2b2b;border:1px solid #989898;box-shadow:5px 5px #989898;padding:1px}
+.postblock{background:#2e8b57;color:#FFF}
+.q{color:#93e0e3}
+.quoted{color:#707070}
+.fs,.info{color:#989898;text-decoration:none}
+.subj{color:#7b68ee;font-weight:bold}
+.name{color:#FBDE2D}
+.name b{color:#989898}
+.omitted,.abbrev{color:#428C29}
+.thread{border:solid 1px #7b68ee;background:#1A1A1A;box-shadow:5px 5px #7b68ee;margin-top:1em;margin-bottom:1em;padding:5px}
+.reply,#q-p{background:#2b2b2b;border:1px solid #2e8b57 !important;box-shadow:5px 5px #2e8b57}
+.thread table{margin:0;margin-bottom:10px;margin-right:10px}
+.thumb{border:1px solid #4D4D4D;box-shadow:3px 3px #4D4D4D;margin-bottom:5px}
+.reply.highlight{border-color:#93e0e3 !important;box-shadow:5px 5px #93e0e3}
+.pg,.userdelete,.nav{background:#2b2b2b;border:1px solid #2e8b57;box-shadow:5px 5px #2e8b57;color:#89A}
+.nav{padding:4px}
+#adminmenu table{margin-bottom:10px}
+.managertable td{background:#2B2B2B;color:#FFF}
+.managertable th{background:#1A1A1A;color:#FFF}
+#catalog .thread{border:1px solid #61CE3C;border-radius:0;box-shadow:none}
+#catalog .thread:hover{background:#383838}
+#catalog .replies{color:#428C29;font-weight:bold}
+.yt{background:#222;border:1px solid;box-shadow:2px 2px}
+.quoted{border-color:#2e8b57;color:#989898} \ No newline at end of file
diff --git a/static/css/dickgirl.css b/static/css/dickgirl.css
new file mode 100644
index 0000000..ba0bcc4
--- /dev/null
+++ b/static/css/dickgirl.css
@@ -0,0 +1,23 @@
+html,body{background:#1B3345;color:#FFF;font-family:initial}
+a,a .name,.nav label{color:#EFD279}
+a.rep{color:#FFF}
+a:hover,a:hover .name,.nav label:hover{color:#D00}
+.logo{color:#CCFFCC}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#1D7548}
+.extramode{background:#0040E0}
+.postblock{background:#95CBE9;color:#2C5700}
+.q{color:#BDF46C}
+.fs{text-decoration:none}
+.subj{color:#DE9D7F;font-weight:bold}
+.name{color:#AFD775}
+.omitted{color:#909090}
+.reply,#q-p{background:#3B6B94;color:#fff}
+.abbrev{color:#BBB}
+.highlight{background:#5B8BB4}
+.managertable td{background:#3B6B94;color:#FFF}
+.managertable th{background:#AAF;color:#FFF}
+#catalog .thread:hover{background:#3B6B94;box-shadow:0 0 5px 5px #3B6B94}
+#catalog .replies{color:#AAA;font-weight:bold}
+.yt{background:#2F587A;border:1px solid #1B2933}
+.quoted{border-color:#1B3345} \ No newline at end of file
diff --git a/static/css/easymodo.css b/static/css/easymodo.css
new file mode 100644
index 0000000..eaa88a8
--- /dev/null
+++ b/static/css/easymodo.css
@@ -0,0 +1,25 @@
+html,body{background:#EEFFF2;color:#000;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+.rep{color:#000}
+a,a .name,a .name b,.nav label{color:#34345c}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover{color:#D00}
+.reflink a:hover{text-decoration:underline}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#E04000}
+.extramode{background:#0040E0}
+.postform,.oekform{background:#cce1cf;border:1px solid #CCE1CF;padding:1px}
+.postblock{background:#98c1a9}
+.q{color:#789922}
+.quoted{color:#707070}
+.fs{text-decoration:none}
+.subj{color:#0f0c5d;font-weight:bold}
+.name{color:#228854}
+.name b{color:#117743}
+.omitted,.abbrev{color:#707070}
+.reply,#q-p,.pg{background:#d6f0da}
+.highlight{background:#d6bad0}
+.managertable td{background:#cce1cf}
+.managertable th{background:#b8caba}
+#catalog .thread:hover{background:#d6f0da;box-shadow:0 0 5px 5px #d6f0da}
+#catalog .replies{color:#707070;font-weight:bold}
+.yt{background:#c2d6c5;border:1px solid #7a877c;color:#000}
+.quoted{border-color:#EEFFF2} \ No newline at end of file
diff --git a/static/css/futaba.css b/static/css/futaba.css
new file mode 100644
index 0000000..f90af88
--- /dev/null
+++ b/static/css/futaba.css
@@ -0,0 +1,22 @@
+html,body{background:#FFE;color:#800000;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+a,a .name,a .name b,.nav label{color:#0000EE}
+a.rep{color:#800000;text-decoration:underline}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover{color:#D00}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#E04000}
+.extramode{background:#0040E0}
+.postblock{background:#EEAA88}
+.q{color:#789922}
+.subj{color:#CC1105;font-weight:bold}
+.name{color:#228854}
+.name b{color:#117743}
+.omitted{color:#707070}
+.reply,#q-p{background:#F0E0D6}
+.abbrev{color:#707070}
+.highlight{background:#F0C0B0}
+.managertable td{background:#EEEECC;color:#800000}
+.managertable th{background:#AAAA66;color:#400000}
+#catalog .thread:hover{background:#F0E0D6;box-shadow:0 0 5px 5px #F0E0D6}
+#catalog .replies{color:#909090;font-weight:bold}
+.yt{background:#F2E5DD;border:1px solid #E7CFC1;color:#500000}
+.quoted{border-color:#FFE} \ No newline at end of file
diff --git a/static/css/guro.css b/static/css/guro.css
new file mode 100644
index 0000000..c938675
--- /dev/null
+++ b/static/css/guro.css
@@ -0,0 +1,24 @@
+html,body{background:#eddad2;color:#000;font-family:arial,helvetica,"nimbus sans l",sans-serif}
+.rep{color:#000}
+a,a .name,a .name b,.nav label{color:#af0a0f}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover{color:#d00}
+.logo{color:#af0a0f}
+.replymode,.extramode{background:#57463f;color:#fff}
+.postblock,.reply{background:#d9af9e;border:1px solid #ca927b}
+input,input[type="text"],input[type="password"],textarea{background:#e6cbc0;border:1px solid #ca927b}
+input[type="submit"],input[type="button"]{border-style:outset;padding:3px 10px}
+input[type="submit"]:active,input[type="button"]:active{border-style:inset}
+.q{color:#707070}
+.subj{color:#0f0c5d;font-weight:bold}
+.name{color:#117743}
+.name b{color:#228854}
+.omitted,.abbrev{color:#444}
+#q-p{background:#d9af9e;border:0}
+.highlight{background:#5b5f69}
+.managertable td{background:#e6cbc0}
+.managertable th{background:#d9af9e}
+hr{border:none;border-top:1px solid #d9af9e;height:0}
+#catalog .thread:hover{background:#d9af9e;box-shadow:0 0 5px 5px #d9af9e}
+#catalog .replies{color:#555;font-weight:bold}
+.yt{background:#c39e8e;border:1px solid #8d6656}
+.quoted{border-color:#ca927b} \ No newline at end of file
diff --git a/static/css/ib.css b/static/css/ib.css
new file mode 100644
index 0000000..1609e4d
--- /dev/null
+++ b/static/css/ib.css
@@ -0,0 +1,72 @@
+*{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;word-wrap:break-word}
+html,body{font-family:arial,helvetica,"nimbus sans l",sans-serif}
+html{margin:0;padding:0}
+body{margin:0;padding:10px;font-size:16px}
+blockquote{line-height:20px}
+.spoil{background:#000;color:#000}.spoil:hover{color:#fff}
+.postarea table{margin:0 auto;text-align:left}
+#main_nav{font-size:14px;line-height:1.3em;margin-bottom:.5em;text-align:center}
+#main_nav span{display:inline-block}
+#main_nav #sel,#main_nav .cur_brd,#main_nav #cur_stl{font-weight:bold}
+.logo{font-size:160%;font-weight:bold;margin:8px 0;text-align:center}
+input[type="text"],input[type="password"],textarea{background:#FFF;border:1px inset gray;padding:3px}
+.postform input{max-width:250px}
+.postform input[name="subject"]{max-width:300px}
+.postform textarea{width:400px;height:125px}
+.postblock{font-weight:bold;padding:3px}
+.oekform{display:inline-block;margin-bottom:.5em}
+.rules{width:468px;font-size:13px}
+.rules ul{margin:4px 0 0;padding:0}
+.rules li{margin-left:1em}
+.thread{margin-right:170px}
+.thread table{border-collapse:collapse;margin:4px 0}
+.reply .info{padding-right:50px}
+.hsbn{font-size:120%}
+.thumb{border:none;float:left;margin:0 20px}
+.ell{font-family:Mona,IPAMonaPGothic,Monapo,'MS PGothic',YOzFontAA97;vertical-align:top;width:20px}
+.deleted{color:#777}
+.reflink a{color:inherit;text-decoration:none}
+.reply .fs,#q-p .fs{margin-left:20px}
+.pg{border:1px solid;border-spacing:2px;display:table}
+.pg span{border:1px inset;display:table-cell}
+.pg span input{padding:.4em 1em}
+.replymode,.extramode{font-weight:bold;text-align:center;padding:2px;margin:3px 0}
+.name em{color:#009;font-style:normal}
+.quoted{border-top:1px solid;clear:both;font-size:12px;line-height:1;margin:0 -1px;padding:4px}
+.thumbpreview{display:inline-block;max-height:50px;height:auto;max-width:75px;width:auto;vertical-align:middle}
+#catalog{text-align:center}
+#cat_search{border:1px inset gray;padding:0;width:125px}
+#catalog .thread{border-radius:4px;display:inline-block;font-size:14px;margin:5px;overflow:hidden;padding:2px;position:relative;vertical-align:top;max-height:275px;width:175px}
+#catalog.enlarged .thread{margin:10px 5px;max-height:375px;width:275px}
+#catalog .thread a img{border-radius:3px;margin-bottom:2px}
+#catalog.enlarged .thread a img{min-width:150px;min-height:150px;max-width:250px;max-height:250px}
+#catalog .subj{font-size:16px}
+#catalog p{margin:0}
+.yt{font-size:12px;display:inline-block;line-height:13px;margin:2px 0;padding:5px;text-decoration:none}
+.yt b{font-size:14px}
+.yt .pvw{width:100px;height:60px;overflow:hidden;float:left;margin-right:5px}
+.yt .pvw img{margin-top:-15px;margin-left:-10px}
+.cut{clear:both}
+.nav label{text-decoration:underline}
+.userdel{float:right;text-align:center;white-space:nowrap}
+.footer{clear:both;margin-top:8px;text-align:center}
+.managertable th,.managertable td{padding:3px}
+#q-p{position:absolute;border:1px dotted gray}
+@media(max-width:900px){.thread{margin-right:0}}
+@media(max-width:720px){
+ body{font-size:15px;padding:4px}
+ .postblock{font-size:14px;white-space:nowrap}
+ .pass,.ell,.thumbmsg{display:none}
+ .thread table,#postform input[type="text"],#postform input[type="submit"],#postform textarea,#postform input[type="file"],.rules{max-width:100%;width:100%}
+ .info,.fs .tt,.omitted{font-size:14px}
+ .date{font-size:13px}
+ .reply .info{padding-right:0}
+ .fs,.rules{font-size:12px}
+ .fs span{display:none}
+ .reply .fs{margin-left:4px}
+ .thumb{margin:0 8px 0 4px;max-width:100px;max-height:100px;width:auto;height:auto}
+ blockquote{margin:8px!important}
+ .userdel{float:none}
+ .pg{margin-top:0.5em;text-align:center;width:100%}
+ .pg span,.pg input{display:block;width:100%}
+} \ No newline at end of file
diff --git a/static/css/img/0back.png b/static/css/img/0back.png
new file mode 100644
index 0000000..231a44d
--- /dev/null
+++ b/static/css/img/0back.png
Binary files differ
diff --git a/static/css/img/0info.png b/static/css/img/0info.png
new file mode 100644
index 0000000..31ce6be
--- /dev/null
+++ b/static/css/img/0info.png
Binary files differ
diff --git a/static/css/img/0pc.png b/static/css/img/0pc.png
new file mode 100644
index 0000000..f894f66
--- /dev/null
+++ b/static/css/img/0pc.png
Binary files differ
diff --git a/static/css/img/ba.gif b/static/css/img/ba.gif
new file mode 100644
index 0000000..fa1d375
--- /dev/null
+++ b/static/css/img/ba.gif
Binary files differ
diff --git a/static/css/img/barra_dulce.png b/static/css/img/barra_dulce.png
new file mode 100644
index 0000000..c872a7e
--- /dev/null
+++ b/static/css/img/barra_dulce.png
Binary files differ
diff --git a/static/css/img/bg_deportes.gif b/static/css/img/bg_deportes.gif
new file mode 100644
index 0000000..1d107e5
--- /dev/null
+++ b/static/css/img/bg_deportes.gif
Binary files differ
diff --git a/static/css/img/bg_madera.png b/static/css/img/bg_madera.png
new file mode 100644
index 0000000..1e98769
--- /dev/null
+++ b/static/css/img/bg_madera.png
Binary files differ
diff --git a/static/css/img/bg_oculto.gif b/static/css/img/bg_oculto.gif
new file mode 100644
index 0000000..65a3744
--- /dev/null
+++ b/static/css/img/bg_oculto.gif
Binary files differ
diff --git a/static/css/img/bgtb.gif b/static/css/img/bgtb.gif
new file mode 100644
index 0000000..c9f675c
--- /dev/null
+++ b/static/css/img/bgtb.gif
Binary files differ
diff --git a/static/css/img/checked.png b/static/css/img/checked.png
new file mode 100644
index 0000000..67332b0
--- /dev/null
+++ b/static/css/img/checked.png
Binary files differ
diff --git a/static/css/img/cyb.png b/static/css/img/cyb.png
new file mode 100644
index 0000000..a75a55f
--- /dev/null
+++ b/static/css/img/cyb.png
Binary files differ
diff --git a/static/css/img/cyba.png b/static/css/img/cyba.png
new file mode 100644
index 0000000..c79e9b2
--- /dev/null
+++ b/static/css/img/cyba.png
Binary files differ
diff --git a/static/css/img/fondo2012.gif b/static/css/img/fondo2012.gif
new file mode 100644
index 0000000..d31e6a3
--- /dev/null
+++ b/static/css/img/fondo2012.gif
Binary files differ
diff --git a/static/css/img/green.gif b/static/css/img/green.gif
new file mode 100644
index 0000000..9d4963c
--- /dev/null
+++ b/static/css/img/green.gif
Binary files differ
diff --git a/static/css/img/hand.png b/static/css/img/hand.png
new file mode 100644
index 0000000..291411c
--- /dev/null
+++ b/static/css/img/hand.png
Binary files differ
diff --git a/static/css/img/luz.gif b/static/css/img/luz.gif
new file mode 100644
index 0000000..f8667cf
--- /dev/null
+++ b/static/css/img/luz.gif
Binary files differ
diff --git a/static/css/img/muro.jpg b/static/css/img/muro.jpg
new file mode 100644
index 0000000..54dd86b
--- /dev/null
+++ b/static/css/img/muro.jpg
Binary files differ
diff --git a/static/css/img/nieve.png b/static/css/img/nieve.png
new file mode 100644
index 0000000..ba8fc25
--- /dev/null
+++ b/static/css/img/nieve.png
Binary files differ
diff --git a/static/css/img/picnicbdy.gif b/static/css/img/picnicbdy.gif
new file mode 100644
index 0000000..f0c30bc
--- /dev/null
+++ b/static/css/img/picnicbdy.gif
Binary files differ
diff --git a/static/css/img/picnicbg.gif b/static/css/img/picnicbg.gif
new file mode 100644
index 0000000..4c6aa64
--- /dev/null
+++ b/static/css/img/picnicbg.gif
Binary files differ
diff --git a/static/css/img/picnicbtm.gif b/static/css/img/picnicbtm.gif
new file mode 100644
index 0000000..a380890
--- /dev/null
+++ b/static/css/img/picnicbtm.gif
Binary files differ
diff --git a/static/css/img/picnicbtn.gif b/static/css/img/picnicbtn.gif
new file mode 100644
index 0000000..b7fd7fe
--- /dev/null
+++ b/static/css/img/picnicbtn.gif
Binary files differ
diff --git a/static/css/img/picnicfg.gif b/static/css/img/picnicfg.gif
new file mode 100644
index 0000000..435ab0f
--- /dev/null
+++ b/static/css/img/picnicfg.gif
Binary files differ
diff --git a/static/css/img/picnichr.gif b/static/css/img/picnichr.gif
new file mode 100644
index 0000000..b50ccb1
--- /dev/null
+++ b/static/css/img/picnichr.gif
Binary files differ
diff --git a/static/css/img/picnicmid.gif b/static/css/img/picnicmid.gif
new file mode 100644
index 0000000..de2beb4
--- /dev/null
+++ b/static/css/img/picnicmid.gif
Binary files differ
diff --git a/static/css/img/picnicthr1.gif b/static/css/img/picnicthr1.gif
new file mode 100644
index 0000000..c9a967f
--- /dev/null
+++ b/static/css/img/picnicthr1.gif
Binary files differ
diff --git a/static/css/img/picnicthr2.gif b/static/css/img/picnicthr2.gif
new file mode 100644
index 0000000..cbe13fb
--- /dev/null
+++ b/static/css/img/picnicthr2.gif
Binary files differ
diff --git a/static/css/img/picnicthr3.gif b/static/css/img/picnicthr3.gif
new file mode 100644
index 0000000..55c22c1
--- /dev/null
+++ b/static/css/img/picnicthr3.gif
Binary files differ
diff --git a/static/css/img/picnictop.gif b/static/css/img/picnictop.gif
new file mode 100644
index 0000000..91ccc5e
--- /dev/null
+++ b/static/css/img/picnictop.gif
Binary files differ
diff --git a/static/css/img/scan.png b/static/css/img/scan.png
new file mode 100644
index 0000000..6632095
--- /dev/null
+++ b/static/css/img/scan.png
Binary files differ
diff --git a/static/css/img/scroller1.gif b/static/css/img/scroller1.gif
new file mode 100644
index 0000000..633d7c1
--- /dev/null
+++ b/static/css/img/scroller1.gif
Binary files differ
diff --git a/static/css/img/tanasinn.gif b/static/css/img/tanasinn.gif
new file mode 100644
index 0000000..466614b
--- /dev/null
+++ b/static/css/img/tanasinn.gif
Binary files differ
diff --git a/static/css/img/vndb1.jpg b/static/css/img/vndb1.jpg
new file mode 100644
index 0000000..f91e414
--- /dev/null
+++ b/static/css/img/vndb1.jpg
Binary files differ
diff --git a/static/css/img/vndb2.jpg b/static/css/img/vndb2.jpg
new file mode 100644
index 0000000..e1dff0c
--- /dev/null
+++ b/static/css/img/vndb2.jpg
Binary files differ
diff --git a/static/css/img/vndb3.png b/static/css/img/vndb3.png
new file mode 100644
index 0000000..d31bc8f
--- /dev/null
+++ b/static/css/img/vndb3.png
Binary files differ
diff --git a/static/css/kraut.css b/static/css/kraut.css
new file mode 100644
index 0000000..a9e82da
--- /dev/null
+++ b/static/css/kraut.css
@@ -0,0 +1,24 @@
+html,body{background:#eee;color:#000;font-family:arial,helvetica,"nimbus sans l",sans-serif}
+.rep{background:#313370;border:1px solid #6569e5;border-radius:3px;color:#bdbee4;display:inline-block;font-size:14px;font-weight:bold;padding:1px;text-decoration:none}
+a,a .name,.nav label{color:#229}
+a:hover,a:hover .name,.nav label:hover{color:#922}
+#main_nav{background:#bbd;margin:-10px -10px 0;padding:10px}
+.replymode,.extramode{background:#313370;color:#fff}
+.postblock{background:#313370;color:#fff}
+.q{color:#077}
+.reply .fs{font-style:italic}
+.fs a{font-style:normal}
+.subj{color:#c33;font-weight:bold}
+.name{color:#33c}
+.omitted,.abbrev{color:#666}
+.reply,#q-p{background:#aac}
+.thumb{background:#ddd;border:1px solid #aaa}
+.reply .thumb{background:#99b;border:1px solid #aaf}
+#q-p{border:1px solid #003099;box-shadow:5px 5px #333}
+.highlight{background:#aac;/*border:2px dashed #448*/}
+.managertable td{background:#ddd}
+.managertable th{background:#313370;color:#fff}
+#catalog .thread:hover{background:#aac;box-shadow:0 0 5px 5px #aac}
+#catalog .replies{color:#888;font-weight:bold}
+.yt{background:#99b;border:1px solid #313370;color:#000}
+.quoted{border-color:#eee;color:#077} \ No newline at end of file
diff --git a/static/css/mobile.css b/static/css/mobile.css
new file mode 100644
index 0000000..9921d3a
--- /dev/null
+++ b/static/css/mobile.css
@@ -0,0 +1,129 @@
+*{box-sizing:border-box;word-wrap:break-word}
+body,input,textarea{color:#000;margin:0 auto;max-width:700px;padding:0}
+body,textarea{font-family:arial,sans-serif;font-size:16px}
+.txt{background:#efefef}
+.img{background:#1a1d22;color:#e0e0e0}
+br{line-height:0.5em}
+a{text-decoration:none}
+.txt a{color:#00c}
+.txt a:active{color:#f00}
+.img a{color:#b0ccde}
+.img a:hover{color:#5c6a74}
+.txt a.num{color:#222}
+.img .num{color:#a3a3a3;float:right}
+h1{font-size:18px}
+.txt h1{margin-left:2px}
+.img h1{color:#a6b8d8;margin:0 0 5px}
+h1 span{font-weight:400}
+h2{color:#a6b8d8;font-size:14px;margin:0;}
+h3,h4{clear:both;font-size:12px;font-weight:400;line-height:1;margin:10px 0}
+h3 em{font-weight:700;font-style:normal}
+h4{text-align:right}
+.txt h3,h4{color:#777}
+.txt h3.del,.txt h3.del a.num{color:#afafaf}
+.img h3{color:#a3a3a3;margin:0 0 5px}
+.img h3.del{color:#606060;margin-bottom:0}
+.prev h3{clear:none;margin:0}
+hr{margin:.25em 0}m
+.img hr{border:none;border-top:1px solid #333;height:0}
+.top{background:#444;color:#fff;font-size:32px;font-weight:700;line-height:1;min-height:30px;padding:10px 2px}
+.top a{color:#fff!important;float:right;font-size:14px;margin-top:-10px;padding-top:10px;text-align:center;height:50px;width:50px}
+.top a img{height:18px;width:18px}
+.bar{font-weight:bold;overflow-y:hidden;overflow-x:auto;white-space:nowrap;width:100%}
+.txt .bar{background:#ccc}
+.img .bar{background:#050607}
+.bar a{padding:8px;display:inline-block}
+.txt .bar a{color:#000}
+.img .bar a{color:#e0e0e0}
+.txt .bar a.sel{background:#efefef}
+.img .bar a.sel{background:#1a1d22}
+.ord{overflow-y:hidden;overflow-x:auto;white-space:nowrap;width:100%}
+.txt .ord{border-top:1px solid #f8f8f8;border-bottom:1px solid #ddd;color:#000}
+.img .ord{border-top:1px solid #444;border-bottom:1px solid #111;color:#e0e0e0}
+.ord span{display:inline-block;font-weight:bold;padding:7px 10px}
+.ord a{display:inline-block;padding:7px 10px}
+.ord a.sel:before{content:'✓ '}
+.txt .ord a{border-left:1px solid #f8f8f8;border-right:1px solid #ddd;color:#000}
+.img .ord a{border-left:1px solid #444;border-right:1px solid #111;color:#e0e0e0}
+.txt .ord a:hover{background:#dcdcdc}
+.img .ord a:hover{background:#14161a}
+.nav{text-align:center}
+.list a{display:block;padding:10px 5px}
+.txt .list a{border-top:1px solid #f8f8f8;border-bottom:1px solid #ddd;color:#000}
+.txt .list a:hover{background:#dcdcdc;color:#000}
+.img .list a{border-top:1px solid #444;border-bottom:1px solid #111;color:#e0e0e0;overflow:hidden}
+.img .list a:hover{background:#14161a}
+.list div{font-size:90%;font-weight:700;margin-top:3px;text-align:right}
+.list div span{font-weight:400}
+.list div span span{font-weight:700;color:red}
+.list .info{display:block;font-size:75%;margin-top:3px}
+.txt .list .info{color:#777}
+.img .list .info{color:#a3a3a3}
+.list .info span{float:right}
+.txt .nav{border-top:1px solid #c6c7c8}
+.img .nav{border-top:1px solid #333}
+.nav div{display:table;width:100%}
+.txt .nav div{border-bottom:1px solid #c6c7c8}
+.img .nav div{border-bottom:1px solid #333}
+.nav div a{display:table-cell;padding:6px 0;width:33%}
+.txt .nav div a,#nav2 a{border-right:1px solid #c6c7c8;color:#000}
+.img .nav div a{border-right:1px solid #333;color:#e0e0e0}
+.txt .nav div a:last-child,.img .nav div a:last-child{border-right:0}
+#nav2{overflow-y:auto;padding:3px;text-align:center;white-space:nowrap;width:100%}
+#nav2 a{border:1px solid #c6c7c8;border-radius:5px;display:inline-block;padding:4px}
+.msg{line-height:1.2em}
+.msg a{border:1px solid;border-radius:10px;display:inline-block;font-size:12px;margin:2px 0;padding:4px}
+.msg a[href^="/"]{border-radius:5px;font-size:inherit;padding:2px 4px}
+.thm{color:grey!important;font-size:12px;float:left;line-height:12px;margin-right:5px;text-align:center}
+.thm img{margin-bottom:2px}
+.mnu{transform:rotate(90deg);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);float:left;font-weight:bold;line-height:1}
+.txt a.mnu{color:#777;position:relative;left:2px}
+.img .mnu{color:#a3a3a3;margin-right:3px}
+#mnu-list{border:1px solid #111;border-bottom:0;box-shadow:1px 1px #000;display:inline-block;position:absolute}
+.txt #mnu-list{background:#E6E6E6}
+.img #mnu-list{background:#2A2D37}
+#mnu-list a{border-bottom:1px solid #111;color:inherit;display:block;padding:5px}
+.txt #mnu-list a:hover{background:#DDD}
+.img #mnu-list a:hover{background:#1E212B}
+.search input{background:#fff;border-top:1px solid #f8f8f8;border-right:0;border-bottom:1px solid #ddd;border-left:0;padding:8px;width:100%}
+.fld,#post,textarea{background:#fff;border:none;width:100%}
+.img .fld,.img textarea{border-bottom:1px solid #1a1d22}
+.txt .fld,.txt textarea{border-bottom:1px solid #efefef}
+input[type="text"].fld{font-size:12px;padding:4px 0}
+input[type="file"].fld{color:#000;padding:2px 0;width:100%}
+input[type="text"].imp{font-size:16px;font-weight:700}
+.file{display:table;width:100%}.file .fld{display:table-cell}
+.file label{color:#000;padding-left:5px;vertical-align:middle;white-space:nowrap;width:auto}
+#post{background:#fefefe;background:-webkit-linear-gradient(#fefefe,#e7e8e9);background:-o-linear-gradient(#fefefe,#e7e8e9);background:-moz-linear-gradient(#fefefe,#e7e8e9);background:linear-gradient(#fefefe,#e7e8e9);border-top:1px solid #c6c7c8;border-bottom:1px solid #c6c7c8;box-shadow:0 1px #fff,0 -1px #fff;display:block;padding:12px 0}
+#post:active{background:#e7e8e9;background:-webkit-linear-gradient(#e7e8e9,#fefefe);background:-o-linear-gradient(#e7e8e9,#fefefe);background:-moz-linear-gradient(#e7e8e9,#fefefe);background:linear-gradient(#e7e8e9,#fefefe)}
+#post:disabled{background:#fefefe;background:-webkit-linear-gradient(#fefefe,#e7e8e9);background:-o-linear-gradient(#fefefe,#e7e8e9);background:-moz-linear-gradient(#fefefe,#e7e8e9);background:linear-gradient(#fefefe,#e7e8e9);color:gray;text-shadow:1px 1px #fff}
+.img form{margin:5px 0}.img #post{margin-top:5px}
+.txt form{margin:10px 0}.txt #post{margin-top:10px}
+.img .cat{border:1px solid #373a44;color:#e0e0e0;display:inline-block;font-size:12px;height:150px;margin:3px;overflow:hidden;position:relative;padding:2px;text-align:center;vertical-align:top;width:130px}
+.img .cat:hover{color:#fff;background:#373a44}
+.img .cat img{margin-bottom:2px}
+.img .prev{border-bottom:1px solid #333;font-size:14px;margin:8px 0;overflow:hidden;padding:0 5px 8px}
+.img .prev .pst{margin-top:5px;margin-bottom:0}
+.img .first,.img .pst{display:block;padding:5px;overflow:hidden}
+.img .pst{background:#373a44;margin-bottom:5px}
+.img .q{color:#789922}
+.img .yt{background:#292c33;border:1px solid #5e6b7d;color:#e0e0e0}
+#n{display:block;padding:6px 0;text-align:center}
+.txt #n{border-top:1px solid #c6c7c8;color:#000}
+.img #n{border-top:1px solid #333;color:#e0e0e0}
+.txt #thread{margin-top:8px}
+.txt .msg a{background:#e9e9e9;border-color:#ccc}
+.txt .msg a:active{border-color:red}
+.txt .pst{border-top:1px solid #c6c7c8;padding:0 2px}
+.txt .q{color:#666}
+.txt .yt{background:#ddd;border:1px solid gray!important}
+.rules{font-size:12px;text-align:center}
+.txt .rules{margin:10px}.img .rules{margin:5px}
+.stop{font-size:75%;line-height:3em;padding:2px}
+.warn{font-size:75%;padding:5px 2px}
+.yellow{background:#ff0;color:#000}
+.red{background:red;color:#fff}
+.yt{border-radius:0!important;line-height:1.2em!important;margin:2px 0;padding:4px!important}
+.yt .pvw{float:left;height:60px;margin-right:4px;overflow:hidden;width:100px}
+.yt .pvw img{margin-top:-15px;margin-left:-10px}
+.yt b{font-size:115%} \ No newline at end of file
diff --git a/static/css/night.css b/static/css/night.css
new file mode 100644
index 0000000..ba3ee7b
--- /dev/null
+++ b/static/css/night.css
@@ -0,0 +1,22 @@
+html,body{background:#171e24;color:#979ea3;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+a,a .name,.nav label{color:#c0c4c8}
+a.rep{color:#979ea3;text-decoration:underline}
+a:hover,a:hover .name,.nav label:hover{color:#888c90}
+.replymode,.extramode{color:#DDD}
+.replymode{background:#B40D00}
+.extramode{background:#001f6e}
+.postblock{background:#2b3843}
+.q{color:#789922}
+.subj{background:inherit;color:#962e5f;font-weight:bold}
+.name{color:#5f962e}
+.name a{color:#2e5f96}
+.name a:hover{color:#426fa0}
+.reply{background:#2F3D48;border:1px solid #1c242b}
+.abbrev{color:#707070}
+.highlight{background:#1D1D21;border:1px solid #111}
+hr{border:none;border-top:1px solid #979ea3;height:0}
+#catalog .thread:hover{background:#2F3D48;box-shadow:0 0 5px 5px #2F3D48}
+#catalog .replies{color:#909090;font-weight:bold}
+.yt{background:#52626D;border:1px solid #131D25}
+#q-p{background:#2F3D48}
+.quoted{border-color:#171e24} \ No newline at end of file
diff --git a/static/css/photon.css b/static/css/photon.css
new file mode 100644
index 0000000..7be906a
--- /dev/null
+++ b/static/css/photon.css
@@ -0,0 +1,22 @@
+html,body{background:#EEE;color:#333;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+.rep{color:#333}
+a,a .name,a .name b,.logo,.nav label{color:#F60}
+a:hover,a:hover .name,a:hover .name b,.nav label:hover{color:#0066FF}
+.replymode,.extramode{color:#2266AA;border:solid 1px #CCC;background:#DDD}
+.postblock{background:#DDD;color:#024;border:1px solid #CCC;}
+.q{color:#789922}
+.fs,.abbrev{color:#666;text-decoration:none}
+.subj{color:#111;font-weight:bold}
+.name{color:#F30}
+.name b{color:#004A99}
+.omitted{color:#707070}
+.reply{background:#DDD;border:1px solid #CCC}
+.highlight{background:#CCC}
+.managertable td{background:#DDD;color:#024}
+.managertable th{background:#CCC;color:#024}
+hr{border:none;border-top:1px solid #BBB;height:0}
+#catalog .thread:hover{background:#DDD;box-shadow:0 0 5px 5px #DDD}
+#catalog .replies{color:#888;font-weight:bold}
+.yt{background:#E9E9E9;border:1px solid #BBB;color:#333}
+#q-p{background:#DDD}
+.quoted{border-color:#CCC} \ No newline at end of file
diff --git a/static/css/putaba.css b/static/css/putaba.css
new file mode 100644
index 0000000..840d5fd
--- /dev/null
+++ b/static/css/putaba.css
@@ -0,0 +1,46 @@
+html,body{background:#fff url('img/fondo2012.gif');font-family:"courier new",courier,monospace;color:#000}
+a,.reflink a,.nav label{color:#fff;text-decoration:none;background:#f60}
+a.rep{text-decoration:underline}
+a:hover,.reflink a:hover,.nav label:hover{background:#f00}
+a .name,a .name b{background:#0cf;color:#903}
+a:hover .name,a:hover .name b{background:#f33;color:#fff}
+input[type="button"],input[type="submit"]{background:#eff931;background:-moz-linear-gradient(top, #eff931 0%, #c9e800 100%);border:1px solid #d3de27;color:#333;padding:3px 10px}
+input,input[type="text"],input[type="password"],textarea,.searchbar input{background:#cf0;border:1px solid #000}
+#main_nav a{color:#000;background:#0cf}
+#main_nav a:hover{color:#fff;background:#90f}
+#main_nav span a{color:#fff;background:#f03}
+#main_nav span a:hover{background:#cf0}
+.replymode,.extramode{background:url('img/bgtb.gif');color:#000}
+hr,.ell{display:none}
+.thread hr{display:block}
+.thread,.userdelete,.postform,.oekform{background:#fff;box-shadow:1px 1px 1px rgba(50, 50, 50, 0.5);margin:0 10px 10px;padding:10px;overflow:auto}
+.postarea table{margin:0 auto 10px}
+.info,.fs{text-transform:uppercase}
+.thread label{background:#ff0}
+.thread label a,.thread label a:hover{background:#00ccff;color:#990033}
+.rules{letter-spacing:-0.5px}
+.rules li{margin-left:0.5em}
+.rules a{background:#f06;color:#fff}
+.rules a:hover{color:#cf0}
+.postblock{background:#ff0}
+.q{color:#789922}
+.subj{font-weight:bold;color:#909;background:#cf0;letter-spacing:-1px}
+.name{color:black;background:white}
+.name b{color:white;background:black}
+.date{background:#ff0}
+.omitted,.hsbn,.hsbn:hover{color:#ff3fff;background:#5500aa;display:inline-block}
+.reply,#q-p{background:#e6e6e6}
+.abbrev{color:#707070}
+.highlight{background:#ccc}
+.userdelete{padding:5px}
+input[type="submit"].psei{background:#ff6600;color:white;border:none;padding:0.4em 1em}
+.nav{float:left}
+.managertable td,.pg{background:#fff}
+.managertable th{background:#ddd}
+#catalog .thread:hover{background:#e6e6e6}
+#catalog .replies{background:#00ffff;color:#909090;font-weight:bold}
+.yt{background:#f7f7f7;border:1px solid #999;color:#000}
+.yt:hover{background:#efefef}
+.footer{background:#000;color:#fff}
+.footer a,.footer a:hover{background:#000}
+.quoted{border-color:#fff} \ No newline at end of file
diff --git a/static/css/red.css b/static/css/red.css
new file mode 100644
index 0000000..ad3159a
--- /dev/null
+++ b/static/css/red.css
@@ -0,0 +1,21 @@
+html,body{font-family:Georgia,"URW Bookman L",serif;background:#FFF2F2;color:#800000}
+a,a .name,.nav label{color:#00E}
+a:hover,a:hover .name,.nav label:hover{color:#D00}
+.rep{color:#800000}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#E04000}
+.extramode{background:#0040E0}
+.postblock{background:#F99}
+.q{color:#789922}
+.name{color:#036}
+.omitted,.abbrev{color:#707070}
+.reply,#q-p{background:#FBB}
+.subj{color:#CC1105;font-weight:bold}
+.highlight{background:#F0E0D6}
+.managertable td{background:#FED0D0}
+.managertable th{background:#FA4A4A;color:#400000}
+#catalog .thread:hover{background:#FBB}
+#catalog .thread:hover{background:#FBB;box-shadow:0 0 5px 5px #FBB}
+#catalog .replies{color:#909090;font-weight:bold}
+.yt{background:#FED8D8;border:1px solid #FE9B9B;color:#500000}
+.quoted{border-color:#FFF2F2} \ No newline at end of file
diff --git a/static/css/rene.css b/static/css/rene.css
new file mode 100644
index 0000000..7126099
--- /dev/null
+++ b/static/css/rene.css
@@ -0,0 +1,22 @@
+html,body{background:#1a1d22;color:#e0e0e0;font-family:arial,helvetica,"nimbus sans l",sans-serif}
+.rep{color:#e0e0e0}
+a,a .name,.nav label{color:#b0ccde}
+a:hover,a:hover .name,.nav label:hover{color:#5c6a74}
+.replymode,.extramode{color:#fff}
+.replymode{background:#2b2b2b}
+.extramode{background:#333}
+.postblock{background:#28282d;border:1px solid #333;color:#ddd}
+.q{color:#789922}
+.fs{text-decoration:none}
+.subj{color:#a6b8d8;font-weight:bold}
+.name,.omitted{color:#a3a3a3}
+.reply,#q-p{background:#373a44}
+.abbrev{color:#777}
+.highlight{background:#5b5f69}
+.managertable td{background:#64697b}
+.managertable th{background:#252830;color:#f8f8f8}
+hr{border:none;border-top:1px dotted #696969;height:0}
+#catalog .thread:hover{background:#373a44;box-shadow:0 0 5px 5px #373a44}
+#catalog .replies{color:#909090;font-weight:bold}
+.yt{background:#292c33;border:1px solid #5e6b7d;color:#e0e0e0}
+.quoted{border-color:#1a1d22;color:#aaa} \ No newline at end of file
diff --git a/static/css/spc/base.css b/static/css/spc/base.css
new file mode 100644
index 0000000..bb30a24
--- /dev/null
+++ b/static/css/spc/base.css
@@ -0,0 +1,269 @@
+/*
+* Skeleton V1.2
+* Copyright 2011, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 6/20/2012
+*/
+
+
+/* Table of Content
+==================================================
+ #Reset & Basics
+ #Basic Styles
+ #Site Styles
+ #Typography
+ #Links
+ #Lists
+ #Images
+ #Buttons
+ #Forms
+ #Misc */
+
+
+/* #Reset & Basics (Inspired by E. Meyers)
+================================================== */
+ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline; }
+ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
+ display: block; }
+ body {
+ line-height: 1; }
+ ol, ul {
+ list-style: none; }
+ blockquote, q {
+ quotes: none; }
+ blockquote:before, blockquote:after,
+ q:before, q:after {
+ content: '';
+ content: none; }
+ table {
+ border-collapse: collapse;
+ border-spacing: 0; }
+
+
+/* #Basic Styles
+================================================== */
+ body {
+ background: #fff;
+ font: 14px/21px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: #444;
+ -webkit-font-smoothing: antialiased; /* Fix for webkit rendering */
+ -webkit-text-size-adjust: 100%;
+ }
+
+
+/* #Typography
+================================================== */
+ h1, h2, h3, h4, h5, h6 {
+ color: #181818;
+ font-family: "Georgia", "Times New Roman", serif;
+ font-weight: normal; }
+ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
+ h1 { font-size: 46px; line-height: 50px; margin-bottom: 14px;}
+ h2 { font-size: 35px; line-height: 40px; margin-bottom: 10px; }
+ h3 { font-size: 28px; line-height: 34px; margin-bottom: 8px; }
+ h4 { font-size: 21px; line-height: 30px; margin-bottom: 4px; }
+ h5 { font-size: 17px; line-height: 24px; }
+ h6 { font-size: 14px; line-height: 21px; }
+ .subheader { color: #777; }
+
+ p { margin: 0 0 20px 0; }
+ p img { margin: 0; }
+ p.lead { font-size: 21px; line-height: 27px; color: #777; }
+
+ em { font-style: italic; }
+ strong { font-weight: bold; color: #333; }
+ small { font-size: 60%; }
+
+/* Blockquotes */
+ blockquote, blockquote p { font-size: 17px; line-height: 24px; color: #777; font-style: italic; }
+ blockquote { margin: 0 0 20px; padding: 9px 20px 0 19px; border-left: 1px solid #ddd; }
+ blockquote cite { display: block; font-size: 12px; color: #555; }
+ blockquote cite:before { content: "\2014 \0020"; }
+ blockquote cite a, blockquote cite a:visited, blockquote cite a:visited { color: #555; }
+
+ hr { border: solid #ccc; border-width: 1px 0 0; clear: both; margin: 30px 0 30px; height: 0; }
+
+
+/* #Links
+================================================== */
+ a, a:visited { color: #333; text-decoration: underline; outline: 0; }
+ a:hover, a:focus { color: #000; }
+ p a, p a:visited { line-height: inherit; }
+
+
+/* #Lists
+================================================== */
+ ul, ol { margin-bottom: 20px; }
+ ul { list-style: none outside; }
+ ol { list-style: decimal; }
+ ol, ul.square, ul.circle, ul.disc { margin-left: 30px; }
+ ul.square { list-style: square outside; }
+ ul.circle { list-style: circle outside; }
+ ul.disc { list-style: disc outside; }
+ ul ul, ul ol,
+ ol ol, ol ul { margin: 4px 0 5px 30px; font-size: 90%; }
+ ul ul li, ul ol li,
+ ol ol li, ol ul li { margin-bottom: 6px; }
+ li { line-height: 18px; margin-bottom: 12px; }
+ ul.large li { line-height: 21px; }
+ li p { line-height: 21px; }
+
+/* #Images
+================================================== */
+
+ img.scale-with-grid {
+ max-width: 100%;
+ height: auto; }
+
+
+/* #Buttons
+================================================== */
+
+ .button,
+ button,
+ input[type="submit"],
+ input[type="reset"],
+ input[type="button"] {
+ background: #eee; /* Old browsers */
+ background: #eee -moz-linear-gradient(top, rgba(255,255,255,.2) 0%, rgba(0,0,0,.2) 100%); /* FF3.6+ */
+ background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.2)), color-stop(100%,rgba(0,0,0,.2))); /* Chrome,Safari4+ */
+ background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Chrome10+,Safari5.1+ */
+ background: #eee -o-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Opera11.10+ */
+ background: #eee -ms-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* IE10+ */
+ background: #eee linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* W3C */
+ border: 1px solid #aaa;
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ color: #444;
+ display: inline-block;
+ font-size: 11px;
+ font-weight: bold;
+ text-decoration: none;
+ text-shadow: 0 1px rgba(255, 255, 255, .75);
+ cursor: pointer;
+ margin-bottom: 20px;
+ line-height: normal;
+ padding: 8px 10px;
+ font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; }
+
+ .button:hover,
+ button:hover,
+ input[type="submit"]:hover,
+ input[type="reset"]:hover,
+ input[type="button"]:hover {
+ color: #222;
+ background: #ddd; /* Old browsers */
+ background: #ddd -moz-linear-gradient(top, rgba(255,255,255,.3) 0%, rgba(0,0,0,.3) 100%); /* FF3.6+ */
+ background: #ddd -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.3)), color-stop(100%,rgba(0,0,0,.3))); /* Chrome,Safari4+ */
+ background: #ddd -webkit-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Chrome10+,Safari5.1+ */
+ background: #ddd -o-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Opera11.10+ */
+ background: #ddd -ms-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* IE10+ */
+ background: #ddd linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* W3C */
+ border: 1px solid #888;
+ border-top: 1px solid #aaa;
+ border-left: 1px solid #aaa; }
+
+ .button:active,
+ button:active,
+ input[type="submit"]:active,
+ input[type="reset"]:active,
+ input[type="button"]:active {
+ border: 1px solid #666;
+ background: #ccc; /* Old browsers */
+ background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.35) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */
+ background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.35)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */
+ background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Chrome10+,Safari5.1+ */
+ background: #ccc -o-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Opera11.10+ */
+ background: #ccc -ms-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* IE10+ */
+ background: #ccc linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* W3C */ }
+
+ .button.full-width,
+ button.full-width,
+ input[type="submit"].full-width,
+ input[type="reset"].full-width,
+ input[type="button"].full-width {
+ width: 100%;
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ text-align: center; }
+
+ /* Fix for odd Mozilla border & padding issues */
+ button::-moz-focus-inner,
+ input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+ }
+
+
+/* #Forms
+================================================== */
+
+ form {
+ margin-bottom: 20px; }
+ fieldset {
+ margin-bottom: 20px; }
+ input[type="text"],
+ input[type="password"],
+ input[type="email"],
+ textarea,
+ select {
+ border: 1px solid #ccc;
+ padding: 6px 4px;
+ outline: none;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+ border-radius: 2px;
+ font: 13px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: #777;
+ margin: 0;
+ width: 210px;
+ max-width: 100%;
+ display: block;
+ margin-bottom: 20px;
+ background: #fff; }
+ select {
+ padding: 0; }
+ input[type="text"]:focus,
+ input[type="password"]:focus,
+ input[type="email"]:focus,
+ textarea:focus {
+ border: 1px solid #aaa;
+ color: #444;
+ -moz-box-shadow: 0 0 3px rgba(0,0,0,.2);
+ -webkit-box-shadow: 0 0 3px rgba(0,0,0,.2);
+ box-shadow: 0 0 3px rgba(0,0,0,.2); }
+ textarea {
+ min-height: 60px; }
+ label,
+ legend {
+ display: block;
+ font-weight: bold;
+ font-size: 13px; }
+ select {
+ width: 220px; }
+ input[type="checkbox"] {
+ display: inline; }
+ label span,
+ legend span {
+ font-weight: normal;
+ font-size: 13px;
+ color: #444; }
+
+/* #Misc
+================================================== */
+ .remove-bottom { margin-bottom: 0 !important; }
+ .half-bottom { margin-bottom: 10px !important; }
+ .add-bottom { margin-bottom: 20px !important; }
+
+
diff --git a/static/css/spc/halloween.css b/static/css/spc/halloween.css
new file mode 100644
index 0000000..6870152
--- /dev/null
+++ b/static/css/spc/halloween.css
@@ -0,0 +1,47 @@
+body,textarea,h2 small,.del a{color:#FA5923}
+body.mainpage,body.threads{background:#000 url('../img/bg_madera.png');}
+body.threadpage{background:#111}
+a,#n2{color:#F00}
+a:active,a:active .name,#n2:active{color:#f60}
+hr{border:0;border-top:1px solid #900;border-bottom: 1px solid #500000}
+input[type=submit]:active,input[type=button]:active,a:active{filter:blur(2px)}
+h1{text-shadow:0 4px 1px #300000,0 6px 1px #400000,0 8px 1px #500000,0 10px 1px #600000,0 12px 1px #700000,0 14px 1px #800000,0 16px 1px #900000,0 18px 1px #A00000,0 20px 1px #B00000,0 22px 1px #C00000,0 24px 1px #D00000,0 26px 1px #E00000,0 28px 1px #F00000,0 30px 1px #FA0000,0 32px 1px #FB0000,0 34px 1px #FC0000,0 36px 1px #FD0000,0 38px 1px #FE0000,0 40px 2px #F00}
+.threads h1{margin-bottom:35px}
+h2{margin-bottom:3px}
+h2 a{color:#32cd32}
+h3{color:#F00}
+h3 span{color:#900}
+#main_nav{background:#000}
+.outerbox{background:#000;border:1px outset #FF4500;color:#FFA500}
+.innerbox{border:1px inset #FF4500}
+#threadlist{background:#330e00;border:1px inset #FF4500}
+.mainpage .thread,#content{background:#111;border:1px outset #914400}
+.name,a .name,.abbrev{color:#f90}
+.name em{color:#111e6c}
+.msg{color:#fa6a39;margin:4px 40px 24px}
+a.thumb{margin-top:4px}
+.q{color:#875384}
+.yt{background:#240000;border:1px solid #FF4500}
+.deleted{color:#592a56;margin-bottom:24px}
+.outerbox input,.outerbox textarea{background:#222;color:#FFA500;border:1px solid #FF4500}
+.outerbox input:focus,.outerbox textarea:focus{background:#401100}
+.outerbox input[type=submit],.outerbox input[type=button]{background:#000;box-shadow:0 0 5px #FF4500}
+.threadpage input,.threadpage textarea,.thread input,.thread textarea{background:#0C050D;border:1px solid #FA5923;color:#FA5923}
+.threadpage input:focus,.threadpage textarea:focus,.thread input:focus,.thread textarea:focus{background:#290c0a}
+.threadpage input[type=submit],.threadpage input[type=button],.thread input[type=submit],.thread input[type=button]{background:#0C050D}
+#q-p{background:#160901;border:1px solid #521;box-shadow:0 0 5px #521}
+#q-p .msg,#q-p a.thumb{margin-bottom:8px}
+#createbox{display:flex}
+#createbox .extrabox{border:1px inset grey;margin:7px 0 7px 7px;float:left;width:40px;height:auto;flex:0 1 40px;-webkit-flex:0 1 40px}
+#createbox .innerbox{flex:0 1 100%;-webkit-flex:0 1 100%}
+form .msg{border:1px dotted #FA5923;background:#000}
+#footer{text-shadow:0 0 2px}
+#content.list{padding:7px}
+#content.list #header div{background:#2d0000;border-top:1px inset #914400}
+#content.list .row:nth-child(odd),#content.grid .row:hover{background:#000}
+#content.list .row div:first-child{border-left:1px inset #914400}
+#content.list .row div:last-child{border-right:1px inset #914400}
+#content.list .row:last-child div{border-bottom:1px inset #914400}
+#content.grid{border:1px outset #914400;padding:1px}
+#content.grid .row{border:1px inset #914400;margin:1px}
+@media screen and (max-width:480px){.msg{margin:4px 20px 12px}} \ No newline at end of file
diff --git a/static/css/spc/layout.css b/static/css/spc/layout.css
new file mode 100644
index 0000000..b99f451
--- /dev/null
+++ b/static/css/spc/layout.css
@@ -0,0 +1,58 @@
+/*
+* Skeleton V1.2
+* Copyright 2011, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 6/20/2012
+*/
+
+/* Table of Content
+==================================================
+ #Site Styles
+ #Page Styles
+ #Media Queries
+ #Font-Face */
+
+/* #Site Styles
+================================================== */
+
+/* #Page Styles
+================================================== */
+
+/* #Media Queries
+================================================== */
+
+ /* Smaller than standard 960 (devices and browsers) */
+ @media only screen and (max-width: 959px) {}
+
+ /* Tablet Portrait size to standard 960 (devices and browsers) */
+ @media only screen and (min-width: 768px) and (max-width: 959px) {}
+
+ /* All Mobile Sizes (devices and browser) */
+ @media only screen and (max-width: 767px) {}
+
+ /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */
+ @media only screen and (min-width: 480px) and (max-width: 767px) {}
+
+ /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */
+ @media only screen and (max-width: 479px) {}
+
+
+/* #Font-Face
+================================================== */
+/* This is the proper syntax for an @font-face file
+ Just create a "fonts" folder at the root,
+ copy your FontName into code below and remove
+ comment brackets */
+
+/* @font-face {
+ font-family: 'FontName';
+ src: url('../fonts/FontName.eot');
+ src: url('../fonts/FontName.eot?iefix') format('eot'),
+ url('../fonts/FontName.woff') format('woff'),
+ url('../fonts/FontName.ttf') format('truetype'),
+ url('../fonts/FontName.svg#webfontZam02nTh') format('svg');
+ font-weight: normal;
+ font-style: normal; }
+*/ \ No newline at end of file
diff --git a/static/css/spc/navidad.css b/static/css/spc/navidad.css
new file mode 100644
index 0000000..4d90c6a
--- /dev/null
+++ b/static/css/spc/navidad.css
@@ -0,0 +1,161 @@
+html, body {
+ background:#004608;
+ color:#fff;
+}
+a {
+ color:#EFD279;
+}
+a.rep {
+ color:#fff;
+}
+a:hover {
+ color:#DD0000;
+}
+.reflink a:hover{
+ font-weight: bold;
+}
+.adminbar {
+ text-align:right;
+ clear:both;
+ float:right;
+}
+.logo {
+ clear:both;
+ text-align:center;
+ font-size:2em;
+ color:#CCFFCC;
+ width:100%;
+}
+.replymode {
+ background:#078B26;
+ text-align:center;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.catalogmode {
+ background:#0040E0;
+ text-align:center;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.rules {
+ /*font-size:0.7em;*/
+ width: 468px;
+ font-size: 10px;
+ font-family: sans-serif;
+}
+.rules li {
+ margin-left: 1em;
+ /*text-indent: 0em;*/
+}
+.postblock {
+ background:#B00000;
+ color:#FFF;
+ font-weight:800;
+}
+.footer {
+ text-align:center;
+ font-size:12px;
+ font-family:serif;
+}
+.passvalid {
+ background:#EEAA88;
+ text-align:center;
+ width:100%;
+ color:#ffffff;
+}
+.dellist {
+ font-weight: bold;
+ text-align:center;
+}
+.delbuttons {
+ text-align:center;
+ padding-bottom:4px;
+
+}
+.managehead {
+ background:#AAAA66;
+ color:#400000;
+ padding:0px;
+}
+.postlists {
+ background:#FFFFFF;
+ width:100%;
+ padding:0px;
+ color:#800000;
+}
+.row1 {
+ background:#EEEECC;
+ color:#800000;
+}
+.row2 {
+ background:#DDDDAA;
+ color:#800000;
+}
+.q {
+ background:inherit;
+ color:#BDF46C;
+}
+.filesize {
+ text-decoration:none;
+}
+.filetitle {
+ background:inherit;
+ font-size:1.3em;
+ color:#E91F1F;
+ font-weight:800;
+}
+.postername {
+ color:#AFD775;
+ font-weight:bold;
+}
+.postertrip {
+ color:#AFD775;
+}
+.oldpost {
+ color:#CC1105;
+ font-weight:800;
+}
+.omittedposts {
+ color:#909090;
+}
+.reply {
+ background: #078B26;
+ color: #fff;
+}
+.replyhl {
+ background: #F0C0B0;
+ color: #800000;
+}
+.replytitle {
+ font-size: 1.2em;
+ color:#CC1105;
+ font-weight:800;
+}
+.commentpostername {
+ color:#117743;
+ font-weight:800;
+}
+.thumbnailmsg {
+ font-size: small;
+ color:#800000;
+}
+
+.abbrev {
+ color:#707070;
+}
+.highlight {
+ /*background:#95CBE9;*/
+ background: #5B8BB4;
+ /*color:#2C5700;*/
+ /*border: 2px dashed #070;*/
+ border: 2px dashed #AFD775;
+}
+.banned {
+ color:#F99C64;
+}
+.administrator {
+ color:#C00 !important;
+}
diff --git a/static/css/spc/skeleton.css b/static/css/spc/skeleton.css
new file mode 100644
index 0000000..049db08
--- /dev/null
+++ b/static/css/spc/skeleton.css
@@ -0,0 +1,242 @@
+/*
+* Skeleton V1.2
+* Copyright 2011, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 6/20/2012
+*/
+
+
+/* Table of Contents
+==================================================
+ #Base 960 Grid
+ #Tablet (Portrait)
+ #Mobile (Portrait)
+ #Mobile (Landscape)
+ #Clearing */
+
+
+
+/* #Base 960 Grid
+================================================== */
+
+ .container { position: relative; width: 960px; margin: 0 auto; padding: 0; }
+ .container .column,
+ .container .columns { float: left; display: inline; margin-left: 10px; margin-right: 10px; }
+ .row { margin-bottom: 20px; }
+
+ /* Nested Column Classes */
+ .column.alpha, .columns.alpha { margin-left: 0; }
+ .column.omega, .columns.omega { margin-right: 0; }
+
+ /* Base Grid */
+ .container .one.column,
+ .container .one.columns { width: 40px; }
+ .container .two.columns { width: 100px; }
+ .container .three.columns { width: 160px; }
+ .container .four.columns { width: 220px; }
+ .container .five.columns { width: 280px; }
+ .container .six.columns { width: 340px; }
+ .container .seven.columns { width: 400px; }
+ .container .eight.columns { width: 460px; }
+ .container .nine.columns { width: 520px; }
+ .container .ten.columns { width: 580px; }
+ .container .eleven.columns { width: 640px; }
+ .container .twelve.columns { width: 700px; }
+ .container .thirteen.columns { width: 760px; }
+ .container .fourteen.columns { width: 820px; }
+ .container .fifteen.columns { width: 880px; }
+ .container .sixteen.columns { width: 940px; }
+
+ .container .one-third.column { width: 300px; }
+ .container .two-thirds.column { width: 620px; }
+
+ /* Offsets */
+ .container .offset-by-one { padding-left: 60px; }
+ .container .offset-by-two { padding-left: 120px; }
+ .container .offset-by-three { padding-left: 180px; }
+ .container .offset-by-four { padding-left: 240px; }
+ .container .offset-by-five { padding-left: 300px; }
+ .container .offset-by-six { padding-left: 360px; }
+ .container .offset-by-seven { padding-left: 420px; }
+ .container .offset-by-eight { padding-left: 480px; }
+ .container .offset-by-nine { padding-left: 540px; }
+ .container .offset-by-ten { padding-left: 600px; }
+ .container .offset-by-eleven { padding-left: 660px; }
+ .container .offset-by-twelve { padding-left: 720px; }
+ .container .offset-by-thirteen { padding-left: 780px; }
+ .container .offset-by-fourteen { padding-left: 840px; }
+ .container .offset-by-fifteen { padding-left: 900px; }
+
+
+
+/* #Tablet (Portrait)
+================================================== */
+
+ /* Note: Design for a width of 768px */
+
+ @media only screen and (min-width: 768px) and (max-width: 959px) {
+ .container { width: 768px; }
+ .container .column,
+ .container .columns { margin-left: 10px; margin-right: 10px; }
+ .column.alpha, .columns.alpha { margin-left: 0; margin-right: 10px; }
+ .column.omega, .columns.omega { margin-right: 0; margin-left: 10px; }
+ .alpha.omega { margin-left: 0; margin-right: 0; }
+
+ .container .one.column,
+ .container .one.columns { width: 28px; }
+ .container .two.columns { width: 76px; }
+ .container .three.columns { width: 124px; }
+ .container .four.columns { width: 172px; }
+ .container .five.columns { width: 220px; }
+ .container .six.columns { width: 268px; }
+ .container .seven.columns { width: 316px; }
+ .container .eight.columns { width: 364px; }
+ .container .nine.columns { width: 412px; }
+ .container .ten.columns { width: 460px; }
+ .container .eleven.columns { width: 508px; }
+ .container .twelve.columns { width: 556px; }
+ .container .thirteen.columns { width: 604px; }
+ .container .fourteen.columns { width: 652px; }
+ .container .fifteen.columns { width: 700px; }
+ .container .sixteen.columns { width: 748px; }
+
+ .container .one-third.column { width: 236px; }
+ .container .two-thirds.column { width: 492px; }
+
+ /* Offsets */
+ .container .offset-by-one { padding-left: 48px; }
+ .container .offset-by-two { padding-left: 96px; }
+ .container .offset-by-three { padding-left: 144px; }
+ .container .offset-by-four { padding-left: 192px; }
+ .container .offset-by-five { padding-left: 240px; }
+ .container .offset-by-six { padding-left: 288px; }
+ .container .offset-by-seven { padding-left: 336px; }
+ .container .offset-by-eight { padding-left: 384px; }
+ .container .offset-by-nine { padding-left: 432px; }
+ .container .offset-by-ten { padding-left: 480px; }
+ .container .offset-by-eleven { padding-left: 528px; }
+ .container .offset-by-twelve { padding-left: 576px; }
+ .container .offset-by-thirteen { padding-left: 624px; }
+ .container .offset-by-fourteen { padding-left: 672px; }
+ .container .offset-by-fifteen { padding-left: 720px; }
+ }
+
+
+/* #Mobile (Portrait)
+================================================== */
+
+ /* Note: Design for a width of 320px */
+
+ @media only screen and (max-width: 767px) {
+ .container { width: 300px; }
+ .container .columns,
+ .container .column { margin: 0; }
+
+ .container .one.column,
+ .container .one.columns,
+ .container .two.columns,
+ .container .three.columns,
+ .container .four.columns,
+ .container .five.columns,
+ .container .six.columns,
+ .container .seven.columns,
+ .container .eight.columns,
+ .container .nine.columns,
+ .container .ten.columns,
+ .container .eleven.columns,
+ .container .twelve.columns,
+ .container .thirteen.columns,
+ .container .fourteen.columns,
+ .container .fifteen.columns,
+ .container .sixteen.columns,
+ .container .one-third.column,
+ .container .two-thirds.column { width: 300px; }
+
+ /* Offsets */
+ .container .offset-by-one,
+ .container .offset-by-two,
+ .container .offset-by-three,
+ .container .offset-by-four,
+ .container .offset-by-five,
+ .container .offset-by-six,
+ .container .offset-by-seven,
+ .container .offset-by-eight,
+ .container .offset-by-nine,
+ .container .offset-by-ten,
+ .container .offset-by-eleven,
+ .container .offset-by-twelve,
+ .container .offset-by-thirteen,
+ .container .offset-by-fourteen,
+ .container .offset-by-fifteen { padding-left: 0; }
+
+ }
+
+
+/* #Mobile (Landscape)
+================================================== */
+
+ /* Note: Design for a width of 480px */
+
+ @media only screen and (min-width: 480px) and (max-width: 767px) {
+ .container { width: 420px; }
+ .container .columns,
+ .container .column { margin: 0; }
+
+ .container .one.column,
+ .container .one.columns,
+ .container .two.columns,
+ .container .three.columns,
+ .container .four.columns,
+ .container .five.columns,
+ .container .six.columns,
+ .container .seven.columns,
+ .container .eight.columns,
+ .container .nine.columns,
+ .container .ten.columns,
+ .container .eleven.columns,
+ .container .twelve.columns,
+ .container .thirteen.columns,
+ .container .fourteen.columns,
+ .container .fifteen.columns,
+ .container .sixteen.columns,
+ .container .one-third.column,
+ .container .two-thirds.column { width: 420px; }
+ }
+
+
+/* #Clearing
+================================================== */
+
+ /* Self Clearing Goodness */
+ .container:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; }
+
+ /* Use clearfix class on parent to clear nested columns,
+ or wrap each row of columns in a <div class="row"> */
+ .clearfix:before,
+ .clearfix:after,
+ .row:before,
+ .row:after {
+ content: '\0020';
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ width: 0;
+ height: 0; }
+ .row:after,
+ .clearfix:after {
+ clear: both; }
+ .row,
+ .clearfix {
+ zoom: 1; }
+
+ /* You can also use a <br class="clear" /> to clear columns */
+ .clear {
+ clear: both;
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ width: 0;
+ height: 0;
+ } \ No newline at end of file
diff --git a/static/css/spc/valentin.css b/static/css/spc/valentin.css
new file mode 100644
index 0000000..bbbc6c1
--- /dev/null
+++ b/static/css/spc/valentin.css
@@ -0,0 +1,170 @@
+html, body {
+ /*background:#940BFE;*/
+ background: url('img/corazoncitos.gif');
+ color:#fff;
+}
+a {
+ color:#EFD279;
+}
+a.rep {
+ color:#fff;
+}
+a:hover {
+ color:#DD0000;
+}
+.reflink a:hover{
+ font-weight: bold;
+}
+.adminbar {
+ text-align:right;
+ clear:both;
+ float:right;
+}
+.logo {
+ clear:both;
+ text-align:center;
+ font-size:2em;
+ color:#CCFFCC;
+ width:100%;
+}
+.replymode {
+ background:#0BBEB8;
+ text-align:center;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.extramode {
+ background:#0040E0;
+ text-align:center;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.rules {
+ /*font-size:0.7em;*/
+ width: 468px;
+ font-size: 11px;
+ font-family: sans-serif;
+}
+.rules li {
+ margin-left: 1em;
+ /*text-indent: 0em;*/
+}
+.postblock {
+ background:#95CBE9;
+ color:#2C5700;
+ font-weight:800;
+}
+.footer {
+ text-align:center;
+ font-size:12px;
+ font-family:serif;
+}
+.passvalid {
+ background:#EEAA88;
+ text-align:center;
+ width:100%;
+ color:#ffffff;
+}
+.dellist {
+ font-weight: bold;
+ text-align:center;
+}
+.delbuttons {
+ text-align:center;
+ padding-bottom:4px;
+
+}
+.managehead {
+ background:#AAAA66;
+ color:#400000;
+ padding:0px;
+}
+.postlists {
+ background:#FFFFFF;
+ width:100%;
+ padding:0px;
+ color:#800000;
+}
+.row1 {
+ background:#EEEECC;
+ color:#800000;
+}
+.row2 {
+ background:#DDDDAA;
+ color:#800000;
+}
+.q {
+ background:inherit;
+ color:#BDF46C;
+}
+.filesize {
+ text-decoration:none;
+}
+.filetitle {
+ background:inherit;
+ font-size:1.3em;
+ color:#DE9D7F;
+ font-weight:800;
+}
+.postername {
+ color:#AFD775;
+ font-weight:bold;
+}
+.postertrip {
+ color:#AFD775;
+}
+.oldpost {
+ color:#CC1105;
+ font-weight:800;
+}
+.omittedposts {
+ color:#909090;
+}
+.reply {
+ background: #FE0BDF;
+ color: #fff;
+}
+.replyhl {
+ background: #F0C0B0;
+ color: #800000;
+}
+.replytitle {
+ font-size: 1.2em;
+ color:#CC1105;
+ font-weight:800;
+}
+.commentpostername {
+ color:#117743;
+ font-weight:800;
+}
+.thumbnailmsg {
+ font-size: small;
+ color:#800000;
+}
+
+.abbrev {
+ color:#707070;
+}
+.highlight {
+ /*background:#95CBE9;*/
+ background: #5B8BB4;
+ /*color:#2C5700;*/
+ /*border: 2px dashed #070;*/
+ border: 2px dashed #AFD775;
+}
+.banned {
+ color:#F99C64;
+}
+.administrator {
+ color:#C00 !important;
+}
+.managertable td {
+ background:#3B6B94;
+ color:#FFF;
+}
+.managertable th {
+ background:#AAF;
+ color:#FFF;
+} \ No newline at end of file
diff --git a/static/css/spc/valentin2.css b/static/css/spc/valentin2.css
new file mode 100644
index 0000000..189611c
--- /dev/null
+++ b/static/css/spc/valentin2.css
@@ -0,0 +1,177 @@
+html, body {
+ background:url('img/hearts1.gif');
+ color:#000;
+}
+a {
+ color:#2D89D0;
+}
+a.rep {
+ color:#000;
+}
+a:hover {
+ color:#DD0000;
+}
+.reflink a:hover{
+ font-weight: bold;
+}
+.adminbar {
+ text-align:right;
+ clear:both;
+ float:right;
+}
+.logo {
+ clear:both;
+ text-align:center;
+ font-size:2em;
+ color:#B35692;
+ width:100%;
+}
+.replymode {
+ background:#1D7548;
+ text-align:center;
+ font-weight: bold;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.extramode {
+ background:#0040E0;
+ text-align:center;
+ font-weight: bold;
+ padding:2px;
+ color:#FFFFFF;
+ width:100%;
+}
+.rules {
+ /*font-size:0.7em;*/
+ width: 468px;
+ font-size: 11px;
+ font-family: sans-serif;
+}
+.rules li {
+ margin-left: 1em;
+ /*text-indent: 0em;*/
+}
+.postblock {
+ background:#FF7BD1;
+ color:#666;
+ font-weight:800;
+}
+.footer {
+ text-align:center;
+ font-size:12px;
+ font-family:serif;
+}
+.passvalid {
+ background:#EEAA88;
+ text-align:center;
+ width:100%;
+ color:#ffffff;
+}
+.dellist {
+ font-weight: bold;
+ text-align:center;
+}
+.delbuttons {
+ text-align:center;
+ padding-bottom:4px;
+
+}
+.managehead {
+ background:#AAAA66;
+ color:#400000;
+ padding:0px;
+}
+.postlists {
+ background:#FFFFFF;
+ width:100%;
+ padding:0px;
+ color:#800000;
+}
+.row1 {
+ background:#EEEECC;
+ color:#800000;
+}
+.row2 {
+ background:#DDDDAA;
+ color:#800000;
+}
+.q {
+ background:inherit;
+ color:#3D343C;
+}
+blockquote blockquote {
+ background:inherit;
+ color:#6D940C;
+}
+.filesize {
+ text-decoration:none;
+}
+.filetitle {
+ background:inherit;
+ font-size:1.3em;
+ color:#DC3D90;
+ font-weight:800;
+}
+.postername {
+ color:#C000D6;
+ font-weight:bold;
+}
+.postertrip {
+ color:#C000D;
+}
+.oldpost {
+ color:#CC1105;
+ font-weight:800;
+}
+.omittedposts {
+ color:#909090;
+}
+.reply {
+ background: url('img/hearts2.jpg');
+ border: 1px solid #BBF;
+ color: #333;
+}
+.replyhl {
+ background: #F0C0B0;
+ color: #800000;
+}
+.replytitle {
+ font-size: 1.2em;
+ color:#DE9D7F;
+ font-weight:800;
+}
+.commentpostername {
+ color:#117743;
+ font-weight:800;
+}
+.thumbnailmsg {
+ font-size: small;
+ color:#000;
+ margin: 0;
+}
+
+.abbrev {
+ color:#707070;
+}
+.highlight {
+ /*background:#95CBE9;*/
+ background: #5B8BB4;
+ /*color:#2C5700;*/
+ /*border: 2px dashed #070;*/
+ border: 2px dashed #AFD775;
+}
+.banned {
+ color:#F99C64;
+}
+.administrator {
+ color:#C00 !important;
+}
+.managertable td {
+ background:#3B6B94;
+ color:#FFF;
+}
+.managertable th {
+ background:#AAF;
+ color:#FFF;
+}
diff --git a/static/css/txt/4am.css b/static/css/txt/4am.css
new file mode 100644
index 0000000..c3f06b4
--- /dev/null
+++ b/static/css/txt/4am.css
@@ -0,0 +1,42 @@
+body{background:#222 url('/bg2.gif');color:#CED1CF}
+body.threadpage{background-image:none}
+a,a .name,#n2{color:#81A2BE;text-decoration:none}
+a:hover,a:focus,#n2:active{text-decoration:underline}
+hr{border:1px inset #111}
+input[type="text"],input[type="submit"],input[type="button"],textarea,button{background:#383838;border:1px solid #4B4E55;color:#e8e8e8}
+input[type="submit"],input[type="button"],button{background:#333;padding:2px 10px}
+input[type="submit"]:active,input[type="button"]:active,button:active{background:#232323}
+h1{color:#dc9656;font-size:26px;margin-top:-5px;text-align:center}
+h2 a{color:#C66}
+h2 span{color:#999}
+h3{color:#ab4642}
+h3 span{color:#999}
+h4{color:#c1c5c2}
+#main_nav{background:#000;color:#7cafc2;z-index:9}
+.banner{-webkit-filter:grayscale(60%);filter:grayscale(60%)}
+.outerbox{background:#111213;border:1px solid #4B4E55;color:#777879}
+#titlebox .threadnav{margin:-6px}
+#threadlist{background:#0e0e0e;border:1px solid #4B4E55}
+.mainpage .thread,#content{background:#1D1F21;border:3px double #4B4E55;color:#CED1CF;opacity:.9}
+.threadpage .thread{color:#CED1CF}
+.name,.abbrev{color:#B5BD68}
+.name em{color:#88f}
+.del a{color:#ab4642}
+h4 .date,.quoted{color:#4e4e4e}
+.msg{margin:4px 40px 24px}
+a.thumb{margin-top:4px}
+.q{color:#666}
+.yt{background:#383838;border:1px solid gray;color:#e8e8e8}
+.yt:hover{text-decoration:none}
+.deleted{color:#999;margin-bottom:12px}
+#q-p{background:#222;border:1px solid #999}
+#q-p .msg,#q-p a.thumb{margin-bottom:8px}
+.lastposts #n2{text-decoration:none}
+.size{color:#ab4642}
+form .msg{background:#383838;border:1px dotted #000}
+#footer{color:#f7ca88}
+.threads h1{text-align:left}
+#content.list #header div{background:#111213}
+#content.list .row:nth-child(odd),#content.grid .row:hover{background:#191B1D}
+#content.grid{border:1px solid #4B4E55;padding:1px}
+#content.grid .row{border:1px solid #4B4E55;margin:1px} \ No newline at end of file
diff --git a/static/css/txt/amber.css b/static/css/txt/amber.css
new file mode 100644
index 0000000..3de2dd5
--- /dev/null
+++ b/static/css/txt/amber.css
@@ -0,0 +1,44 @@
+body{background:#000;text-shadow:1px 1px #000,0 0 10px}
+body,a,h2 a,h3,#n2{color:#f9690e}
+a:active,#n2:active{color:#FF9F3F}
+h4{background:#f9690e;color:#000;padding:0 7px;text-shadow:none}
+.threadpage h4{padding:0 3px}
+h4 a,.name,.name a{color:#000}
+h1:after,h2 a:after,h3:after{content:"_";animation:blink 1s infinite}@keyframes blink{from,to{opacity:0}50%{opacity:1}}
+h2,.thread .threadnav{margin:7px}
+h3{margin:8px 0}
+h3 span,.size,.abbrev{color:#f00}
+hr{background:#f9690e;border:0;height:1px;box-shadow:0 0 10px #f9690e}
+.spoil{background:#f9690e;color:#f9690e;text-shadow:none}.spoil:hover{color:#000}
+input[type=submit],input[type=button],button{padding:2px 10px}
+input,button{font-family:monospace}
+input,input[type="text"],textarea,button{background:#000;border:1px solid #f9690e;color:#f9690e;text-shadow:1px 1px #000}
+input[type="text"]:focus,textarea:focus{background:#3a1903}
+input[type=submit]:active,input[type=button]:active,button:active{background:#f9690e;color#000}
+#main_nav{background:#000;box-shadow: 0 0 10px}
+.outerbox,.innerbox{border:1px solid #f9690e}
+#threadlist{border:3px double #f9690e}
+.mainpage .thread,.thread .innerbox{border:1px solid #f9690e}
+.thread .innerbox,.threadpage,#q-p{background:#000 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gkRBxEEIxazLgAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAGklEQVQI12NkYGD4z4AEmBjQAAsvK6oAhgoAQ6UBGwQB8rcAAAAASUVORK5CYII=')}
+.thread .innerbox{padding:0}
+.name em{color:#ff0}
+.q{color:#d4b300}
+.deleted{background:none;border-color:#f9690e;border-style:solid;border-width:1px 0;color:inherit}
+.yt{background:#000;border:1px solid #f9690e}
+.mainpage .postform{border-top:1px solid #f9690e;padding:1em 0 1em 40px}
+.formpad{padding:0}
+form .msg{border:1px dotted #f9690e;background:#000;color:#f9690e}
+#q-p{border:1px solid #000;padding:0}
+#q-p .reply{border:3px double #f9690e}
+#q-p .reply h4{padding:1px}
+#footer a{color:#f9690e}
+#content{border:3px double #f9690e}
+#content.list #header{background:#451d04}
+#content.list .row:nth-child(odd){background:#251002}
+#content.grid{border:1px solid #f9690e}
+#content.grid .row{border:1px solid #f9690e}
+#content.grid .row:hover{background:#251002;border-color:#f9690e}
+@media(max-width:720px){
+ div.msg{margin:.5em}
+ .mainpage .postform{padding:.5em}
+} \ No newline at end of file
diff --git a/static/css/txt/ayashii.css b/static/css/txt/ayashii.css
new file mode 100644
index 0000000..b7e5fc0
--- /dev/null
+++ b/static/css/txt/ayashii.css
@@ -0,0 +1,52 @@
+body{background:#004040;color:#FFF}
+a,a .name,#n2:active{color:#EFE}
+a:active,a:active .name,#n2:active{color:#F00}
+h2:before{content:"â—† "}
+h2{display:inline-block;margin:0;margin-bottom:.5em}
+h2 span{font-weight:initial;font-size:24px;margin-left:4px;display:inline-block;float:right}
+h3{margin-bottom:.5em;padding-bottom:.5em}
+h4:before{content:">";color:#FFFFFE;display:inline-block;font-size:16px;width:40px}
+.first h4:before{content:none}
+input[type=submit],input[type=button],button{border:1px outset #FFF;background:#DDD;color:#000;padding:2px 10px}
+input[type=submit]:active,input[type=button]:active,button:active{border-style:inset}
+#main_nav,.banner{background:inherit;text-align:left;padding-left:2.5%}
+#titlebox{margin-bottom:1em;margin-top:5px}
+.threadnav a{margin-left:4px}
+.innerbox{margin:0;padding:0}
+.innerbox,.mainpage .thread,.deleted,#content{margin-bottom:1em;padding-bottom:1em}
+.innerbox,.mainpage .thread,.reply,.deleted,h3,#content{border-bottom:2px groove gray}
+.links,#listmenu{text-align:left}
+#threadbox{margin-bottom:1em}
+#threadlinks{text-align:left;margin-bottom:.5em}
+#threadlist{background:#003535;border:1px inset gray}
+.reply{clear:both;margin-bottom:1em;overflow:hidden}
+.name em{color:#009}
+.quoted{font-size:9pt}
+.del a{color:#FFF}
+.first .msg{margin:1em 40px}
+.msg{margin:1em 60px}
+.q{color:#99b3b3}
+.yt{border:1px solid #FFF}
+.abbrev{margin-top:1em}
+.deleted{text-decoration:line-through}
+#q-p{background:#004040;border:1px solid #DDD}
+#q-p h4:before{display:none}
+#q-p .reply:last-child{border:0;margin-bottom:0}
+.postform,.formpad{padding-left:0}
+.size{color:#FFF;margin-top:-6px}
+form .msg{border:1px dotted #FFF}
+#footer{margin-bottom:1em;margin-top:1em;text-align:right}
+.threads .outerbox{margin-top:1em}
+#content.list #header div{background:#001e1e}
+#content.list .row:nth-child(odd){background:#003535}
+#content.grid{border-width:2px 2px 0 0}
+#content.grid .row{border-width:0 0 2px 2px}
+#content.grid,#content.grid .row{border-style:groove;border-color:gray}
+.threads #footer{text-align:center}
+@media(max-width:720px){
+ body.mainpage,body.threads{margin:8px}
+ h2 span{margin-left:0;float:none}
+ .reply{margin-bottom:.5em}
+ h4:before{font-size:12px;width:20px}
+ div.msg{margin:.5em 0 .5em 20px}
+} \ No newline at end of file
diff --git a/static/css/txt/baisano.css b/static/css/txt/baisano.css
new file mode 100644
index 0000000..82043dd
--- /dev/null
+++ b/static/css/txt/baisano.css
@@ -0,0 +1,43 @@
+body,textarea{color:#000}
+body.mainpage,body.threads{background:#c5ad99 url(../img/muro.jpg)}
+body.threadpage{background:#EFEFEF}
+a,a .name,#n2{color:#00F}
+a:active,a:active .name,#n2:active{color:#f00}
+h2{margin-bottom:3px}
+h2 a{color:#F00}
+h3{color:#F00}
+h3 span{color:#000}
+.mainpage h4:hover{background:#FFEFEF}
+h4.hidden{background:#CCC}
+.mainpage h4.hidden:hover{background:#DCC}
+#main_nav{background:#FFF}
+.outerbox{background:#CFC;border:1px outset #FFF}
+.innerbox{border:1px inset #FFF}
+#threadlist{background:#BEB;border:1px inset #FFF}
+.mainpage .thread,#content{background:#EFEFEF;border:1px outset #FFF}
+.name,.abbrev{color:green}
+.name em{color:#009}
+.del a{color:#000}
+.msg{margin:4px 40px 24px}
+a.thumb{margin-top:4px}
+.q{color:#666}
+.yt{background:#DDD;border:1px solid #AAA}
+.deleted{color:#AFAFAF;margin-bottom:24px}
+#q-p{background:#EFEFEF;border:1px solid #999}
+#q-p .msg,#q-p a.thumb{margin-bottom:8px}
+#createbox{display:flex}
+#createbox .extrabox{border:1px inset #FFF;margin:7px 0 7px 7px;float:left;width:40px;height:auto;flex:0 1 40px;-webkit-flex:0 1 40px}
+#createbox .innerbox{flex:0 1 100%;-webkit-flex:0 1 100%}
+form .msg{border:1px dotted #000;background:#EFEFEF}
+#footer{color:#333}
+#content.list{padding:7px}
+#content.list #header div{background:#CCC;border-top:1px inset #FFF}
+#content.list .row:nth-child(odd),#content.grid .row:hover{background:#FFF}
+#content.list .row div:first-child{border-left:1px inset #FFF}
+#content.list .row div:last-child{border-right:1px inset #FFF}
+#content.list .row:last-child div{border-bottom:1px inset #FFF}
+#content.grid{border:1px outset #FFF;padding:1px}
+#content.grid .row{border:1px inset #FFF;margin:1px}
+@media(max-width:720px){
+ .extrabox{display:none}
+} \ No newline at end of file
diff --git a/static/css/txt/bbs.css b/static/css/txt/bbs.css
new file mode 100644
index 0000000..0ae6041
--- /dev/null
+++ b/static/css/txt/bbs.css
@@ -0,0 +1,95 @@
+*{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;word-wrap:break-word}
+body,textarea{font-family:arial,helvetica,sans-serif;font-size:16px}
+body.mainpage,body.threads{margin:0}
+body.threadpage{margin:8px}
+h1,h5{font-size:18px;margin:0}
+h2{font-size:16px;margin:0 0 7px}
+h2 span{margin-right:2px}
+h2 a{font-size:24px;text-decoration:none}
+h3{font-size:19px;font-weight:normal;margin:8px 0}
+h4{clear:both;font-weight:normal;margin:0}
+h5{margin-bottom:8px}
+input[type="text"],textarea{background:#FFF;border:1px inset #DDD;color:#000}
+.banner{display:block;margin:0 auto}
+#rules{line-height:1.25em;margin:8px 0}
+.spoil{background:#000;color:#000}.spoil:hover{color:#fff}
+code{font-size:14px}
+pre{line-height:125%;margin:8px 0;white-space:pre-wrap}
+#main_nav .cur_brd,#cur_stl,.locked{font-weight:bold}
+#main_nav{font-size:14px;padding:4px 0;text-align:center;width:100%}
+#main_nav a,#thread_nav a,#threadlinks a,.threadlinks a,label{display:inline-block}
+.outerbox,.mainpage .thread,#footer,#content{margin-bottom:1em;margin-left:2.5%;margin-right:2.5%}
+.innerbox{padding:7px;margin:7px}
+.threadnav{float:right;font-family:mona,monapo,ipamonapgothic,monapo,'ms pgothic',yozfontaa97}
+#search input{margin-right:2px}
+#search input[type="text"]{padding:4px;max-width:500px;width:100%}
+.links{font-size:14px;text-align:center}
+#threadlinks{margin-bottom:7px;text-align:center}
+#threadlinks a{margin:0 4px}
+#threadlist{font-size:14px;line-height:1;max-height:11em;padding:7px;overflow-y:scroll}
+#threadlist a{text-decoration:none}
+#thread_nav{padding-left:6px}
+.mainpage .postform,.formpad{padding-left:40px}
+.reply{clear:both;line-height:1.25em;overflow:hidden}
+.num{color:inherit!important;font-weight:bold;text-decoration:none}
+.name em{font-style:normal}
+.del{visibility:hidden}
+.reply:hover .del{visibility:visible}
+.quoted{font-size:12px;line-height:1}
+.msg{margin:1em 40px}
+.msg hr{margin-left:0;max-width:500px}
+a.thumb{cursor:zoom-in;font-size:14px;float:left;margin:1em 20px;text-align:center;text-decoration:none;color:gray!important}
+a.yt{margin:2px 0;display:inline-block;font-size:12px;line-height:1.2em;padding:5px;text-decoration:none}
+a.yt .pvw{width:100px;height:60px;overflow:hidden;float:left;margin-right:5px}
+a.yt .pvw img{margin-top:-15px;margin-left:-10px}
+a.yt b{font-size:115%}
+.deleted{margin-bottom:1em}
+#q-p{margin-right:1em;padding:8px 8px 0;position:absolute}
+#q-p .del,#q-p .quoted,#q-p .thumb div{display:none}
+.size{color:#f00;font-family:arial,sans-serif;font-weight:bold;margin-bottom:8px}
+.lastposts{text-align:center;line-height:1}
+.lastposts #n2{text-decoration:underline}
+.threadpage .threadlinks{margin:8px 0}
+#createbox input,#createbox textarea{width:100%}
+.pblock{text-align:right;white-space:nowrap}
+form .msg{margin:0;padding:8px 2px;text-align:left}
+.end,#search{margin-top:8px}
+.warn{padding:1px}
+.stop{padding:1em 1px}
+.yellow{background:#ff0;color:#000}
+.red{background:#f00;color:#fff}
+#listmenu{margin-top:7px;text-align:center}
+#content a{text-decoration:none}
+#content a:hover{text-decoration:underline}
+#content.list a{display:block;line-height:1}
+#content.list .row{display:table-row}
+#content.list .row div{display:table-cell;padding:5px}
+#content.list .row div.thread{padding:0}#content.list .row div.thread a{padding:5px}
+#content.list .pos,#content.list .com{text-align:right}
+#content.list #header div{font-weight:bold;text-align:center;white-space:nowrap}
+#content.grid{display:flex;flex-flow:row wrap;justify-content:space-around}
+#content.grid .row{flex:1 1 auto;padding:5px}
+#content.grid .row div{display:inline}
+@media(min-height:999px){#threadlist{max-height:16em}}
+@media(max-width:720px){
+ body{font-size:15px}
+ .banner{width:100%!important;height:auto!important;margin-top:1em}
+ .outerbox,.mainpage .thread,#footer,#content{margin:1em 0}
+ br{line-height:.5em}
+ h2 a{font-size:19px}
+ h4,h2 span,#rules{font-size:12px}
+ .pblock,#footer,.end{font-size:14px}
+ #threadlist{overflow-x:auto;white-space:nowrap}
+ .size,.postform br{display:none}
+ .num{margin:-10px;padding:10px;position:relative}
+ .del{visibility:visible}
+ div.msg{margin:.5em 0}
+ .mainpage .postform,.formpad,#thread_nav{padding-left:0}
+ .postform>span{display:table;width:100%}
+ .postform span>*{display:table-cell;font-size:14px;margin-bottom:2px;white-space:nowrap}
+ .postform input,textarea{width:100%}
+ .postform textarea{height:100px}
+ form,#thread_nav,#footer,.end,.locked{text-align:center}
+ #footer a{display:block}
+ #content.list .row div{padding:0 2px}
+} \ No newline at end of file
diff --git a/static/css/txt/bios.css b/static/css/txt/bios.css
new file mode 100644
index 0000000..41e7f38
--- /dev/null
+++ b/static/css/txt/bios.css
@@ -0,0 +1,51 @@
+body,textarea,.lastposts #counter{background:#00a;color:#fff}
+a,a .name,.lastposts #n2{color:#ff0;text-decoration:none}
+a:focus,a:active,a:active .name,#n2:active{background:#f00;color:#fff}
+.mainpage .threadlinks a:before,#threadlinks a:before,.lastposts a:before,.lastposts label:before{content:'â–¶ '}
+input[type="text"],textarea{background:#00a;border:1px solid #fff;color:#fff}
+input[type="submit"],input[type="button"],button{background:#fff;border:0;color:#000;padding:3px 11px}
+input[type="submit"]:active,input[type="button"]:active,button:active{background:#000;color:#fff}
+::selection{background:#fff;color:#000}
+::-moz-selection{background:#fff;color:#000}
+hr{display:none}
+h2,.thread .threadnav{margin:.75em}
+h3{background:#fff;color:#00a;margin:0;padding:2px 8px}
+h3 span{background:#00a;color:#fff;padding:2px}
+#main_nav,#thread_nav{background:linear-gradient(270deg, #0000ff, #000000, #0000ff);background-position:0% 50%;-webkit-animation:bios 6s linear infinite;-moz-animation:bios 6s linear infinite;animation:bios 6s linear infinite;background-size:500% 500%}
+@-webkit-keyframes bios{0%{background-position:0% 0%}50%{background-position:250% 0%}100%{background-position:500% 0%}}
+@-moz-keyframes bios{0%{background-position:0% 0%}50%{background-position:250% 0%}100%{background-position:500% 0%}}
+@keyframes bios{0%{background-position:0% 0%}50%{background-position:250% 0%}100%{background-position:500% 0%}}
+#main_nav a,#thread_nav a,.threadpage .threadlinks a{color:#fff;text-decoration:underline}
+#main_nav .cur_brd{background:#fff;color:#00a;padding:1px;text-decoration:none}
+#thread_nav{margin:-8px -8px 8px;padding:6px 16px}
+.innerbox{border:1px solid #fff;margin:3px;padding:.75em}
+.innerbox.links{border-top:0;margin-top:-3px}
+#cur_stl{background:#ff0;color:#00a}
+.outerbox,.mainpage .thread,#threadlist,#content{border:1px solid #fff}
+.thread .innerbox{padding:0}
+.threadpage .thread{border-color:#fff;border-style:double;border-width:4px 4px 1px}
+.reply{border-top:1px solid #FFF;padding:.5em .75em 0}
+.threadpage .reply{padding:.5em .5em 0}
+.name em{background:#fff;color:#00f}
+.q,.deleted,.abbrev{color:#5cf;margin-bottom:24px}
+.size{margin-left:.5em}
+.yt{border:1px solid #ff0}
+.yt:active{background:#f00;border:1px solid #f00;color:#fff}
+#q-p{background:#00a;border:1px solid #fff}
+#q-p .msg,#q-p a.thumb{margin-bottom:8px}
+.lastposts{border-color:#fff;border-style:double;border-width:0 4px;color:#00a;padding:.5em}
+.mainpage .threadlinks{margin-top:.75em}
+.threadpage .threadlinks{margin-top:0}
+.threadpage .postform{border-color:#fff;border-style:double;border-width:1px 4px 4px;padding:.5em}
+.formpad{padding-left:0}
+.mainpage .postform{border-top:1px solid #fff;padding:.75em}
+form .msg{border:3px double #fff}
+#content.list{border:4px double #fff}
+#content.list #header div{border-bottom:1px solid #fff}
+#content.grid{border:1px solid #fff;padding:2px}
+#content.grid .row{border:1px solid #fff;margin:2px}
+@media(max-width:720px){
+ body.threadpage{margin:8px 0}
+ h2{margin-bottom:.5em}
+ #thread_nav{margin:-8px 0px 8px}
+} \ No newline at end of file
diff --git a/static/css/txt/blue moon.css b/static/css/txt/blue moon.css
new file mode 100644
index 0000000..271fe9a
--- /dev/null
+++ b/static/css/txt/blue moon.css
@@ -0,0 +1,61 @@
+body.threadpage{margin-top:0}
+body{background:#6B7B8D;color:#FFF}
+a,#n2:active,h3 span{color:#AFB9C5}
+a:active,form a:active,#n2:active{color:#EEE}
+#main_nav a,.reply a,form a,#q-p a,.row a{color:#49525D}
+#main_nav a:active,.reply a:active,#q-p a:active,a:active .name,.row a:active{color:#8C9FB4}
+h2{color:#EEE;line-height:1;margin:0 0 10px}
+h2 a{color:#EEE}
+h3{color:#EEE;margin:0.7em 0}
+h4{background:#DDD;padding-bottom:5px}
+hr{display:none}
+input[type=text],textarea{background:#EEE;border:1px inset #444}
+input[type=submit],input[type=button],button{background:#000;color:#FFF;border:1px outset #444;padding:2px 10px}
+input[type=submit]:active,input[type=button]:active,button:active{border-style:inset}
+#main_nav{background:#FFF;color:#6B7B8D}
+#titlebox .innerbox:first-child{margin-bottom:5px}
+.outerbox{background:#49525D;padding:5px}
+.innerbox{margin:0;padding:10px}
+.links{padding-top:5px}
+#threadbox{margin-bottom:19px;margin-top:19px}
+#threadlist{background:#3e4247;border:1px inset #B6C2CF}
+.mainpage .thread{background:#49525D}
+.threadpage .thread{background:#49525D;padding:1px 10px}
+#thread_nav{background:#49525D;margin-bottom:2px;padding:7px 10px}
+.postform,#createbox form,h5{background:#8C9FB4;color:#49525D;padding:10px}
+h5{margin:0;padding-bottom:0}
+.mainpage .postform{margin-top:10px}
+.reply{background:#FFF;border-top:3px solid #DDD;border-right:10px solid #DDD;border-bottom:3px solid #DDD;border-left:10px solid #DDD;color:#000}
+.first{border-top:6px solid #DDD}
+.name,a .name{color:#2B3037}
+.name em{color:#009}
+.del a{color:#000}
+.msg,a.thumb{background:#FFF;color:#000080;margin:10px}
+.msg hr{display:block}
+.abbrev a{color:#000080}
+.abbrev,.q{color:#697685}
+.threadpage .postform{margin-top:2px;border:10px solid #49525D}
+.yt{background:#EEE;border:1px solid gray}
+.deleted{background:#DDD;color:#AFAFAF;margin-bottom:0;padding:3px 10px}
+#q-p{background:#FFF;color:#000;border:1px solid #49525D;padding:0}
+form .msg{border:1px inset #DDD;margin:0;padding:.5em 0.15em}
+.size{color:#8C9FB4;margin:5px 0}
+.lastposts{background:#49525D;color:#AFB9C5;margin-top:2px;padding:.5em}
+.threadpage .threadlinks{margin:0}
+.warn,.stop{padding-left:10px;margin:2px 0}
+#footer a{color:#FFF}
+#content{background:#FFF;border:10px solid #49525D;color:#000080}
+#content.list #header div{background:#2c2c32;color:#EEE}
+#content.list .row:nth-child(odd),#content.grid .row:hover{background:#E7E7FF}
+#content.grid .row{border:1px solid #DDD}
+@media(max-width:720px){
+ body.threadpage{margin:0 0 8px}
+ .innerbox,.postform,#createbox form,h5{padding:6px;padding-left:6px}
+ h2{margin-bottom:6px}
+ .threadpage .thread{padding:1px 6px 6px}
+ .mainpage .postform{margin-top:6px;padding-left:6px}
+ .reply{border-left-width:6px;border-right-width:6px}
+ .threadpage .postform{border-width:6px}
+ .msg,a.thumb{margin:6px}
+ .thread form{margin-top:6px}
+} \ No newline at end of file
diff --git a/static/css/txt/ciber.css b/static/css/txt/ciber.css
new file mode 100644
index 0000000..45fce17
--- /dev/null
+++ b/static/css/txt/ciber.css
@@ -0,0 +1,55 @@
+body{background:#000 url('../img/cyb.png');text-shadow:1px 1px #000}
+body,h3{color:#BA55D3}
+a,h2 a,#n2{color:#DB43B5}
+a:active,#n2:active{color:#F0F}
+h4{background:#2A4A75;padding:0 7px;text-shadow:0 2px #000}
+.threadpage h4{padding:0 3px}
+h4,h4 a,.name,.name a{color:#9370DB}
+h1,h2 a,h3,h5{text-shadow:1px 1px #000,0 0 10px}
+h1:after,h2 a:after,h3:after{content:"_";animation:blink 1s infinite}@keyframes blink{from,to{opacity:0}50%{opacity:1}}
+h2,.thread .threadnav{margin:7px}
+h3{margin:8px 0}
+h3 span,.size,.abbrev{color:#639}
+hr{background:#2A4A75;border:0;height:1px;box-shadow:0 0 10px #2A4A75}
+.spoil{background:#2A4A75;color:#2A4A75;text-shadow:none}.spoil:hover{color:#000}
+input[type=submit],input[type=button],button{padding:2px 10px}
+input:active,input:focus,textarea:active,textarea:focus,button:active,button:active{box-shadow:0 0 15px}
+.outerbox input,.outerbox textarea{background:#002f00;border:1px solid #68F855;color:#68F855}
+.outerbox input[type=submit],.outerbox input[type=button],.outerbox button{background:#0F0;border:3px outset #0F0;color:#000}
+.outerbox input[type=submit]:active,.outerbox input[type=button]:active,.outerbox button:active{background:#0C0;border-style:inset}
+.threadpage input,.threadpage textarea,.thread input,.thread textarea{background:#000;border:1px solid #2A4A75;color:#2A4A75;text-shadow:1px 1px #000}
+.threadpage input[type=submit]:active,.threadpage input[type=button]:active,.thread input[type=submit]:active,.thread input[type=button]:active{border-color:#d21cea;color:#d21cea}
+#main_nav{background:#000;box-shadow:0 0 10px #68F855}
+.outerbox{background:#111;color:#5FE44E;border:3px outset #0F0;background:#0C0}
+.outerbox a,#main_nav a,#footer a{color:#68F855}
+.outerbox a:active,#main_nav a:active,#footer a:active{color:#ef6177}
+#threadlist{background:rgba(26,26,26,.5);border:3px solid #0C0}
+.innerbox{border:3px inset #0F0}
+.outerbox .innerbox{background:#000 url('../img/green.gif')}
+.mainpage .thread,.thread .innerbox{border:1px solid #2A4A75}
+.mainpage .thread{background:#2A4A75;box-shadow:0 0 5px 5px #2A4A75}
+.thread .innerbox,.threadpage,#q-p,form .msg{background:#000 url(' data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAIAAAAW4yFwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAWSURBVHjaYrDk0GLm/88FAAAA//8DAATaAYeGqOFRAAAAAElFTkSuQmCC')}
+.thread .innerbox{padding:0}
+.name em{color:#4B0082}
+.q{color:#9932CC}
+.deleted{background:none;border-color:#2A4A75;border-style:solid;border-width:1px 0;color:inherit}
+.yt{background:#000;border:1px solid #2A4A75}
+.mainpage .postform{border-top:1px solid #2A4A75;padding:1em 0 1em 40px}
+.formpad{padding:0}
+form .msg{border:1px dotted #2A4A75;color:#2A4A75}
+#q-p{border:1px solid #000;padding:0}
+#q-p .reply{border:3px double #2A4A75}
+#q-p .reply h4{padding:1px}
+#footer{color:#0C0;text-shadow:0 0 5px #0F0}
+#footer a{color:#989898}
+#content{background:#000;border:3px double #d500fb;color:#39c8df}#content a{color:#d500fb}
+#content.list #header{text-shadow:0 0 5px}
+#content.list .row:nth-child(odd){background:#200020}
+#content.grid{border:2px solid #141937}
+#content.grid .row{border:2px solid #141937}
+#content.grid .row:hover{background:#27151D;border-color:#9B1937}
+#content .row:hover a{text-shadow:0 0 5px}
+@media(max-width:720px){
+ div.msg{margin:.5em}
+ .mainpage .postform{padding:.5em}
+} \ No newline at end of file
diff --git a/static/css/txt/futanari.css b/static/css/txt/futanari.css
new file mode 100644
index 0000000..2606f03
--- /dev/null
+++ b/static/css/txt/futanari.css
@@ -0,0 +1,49 @@
+body{background:#13334c;color:#edf9fc}
+body.threadpage,body.threads{margin-top:0}
+a,a .name,#n2{color:#efd279}
+a:active,a:active .name,#n2:active{color:#d00}
+h1{background:#5b8bb4;margin:-10px -10px 10px;padding:10px}
+h2 span{color:#aaa;margin-right:2px}
+h2 a,h2 a:active{color:#de9d7f;text-decoration:none}
+h3{color:#de9d7f;margin:0.7em 0}
+h3 span{color:#909090}
+hr{display:none}
+input,input[type="text"],textarea,button{border:1px solid #3b6b94}
+input[type=submit],input[type=button],button{background:#aaf;color:#2c5700;padding:2px 10px}
+input[type="submit"]:active,input[type="button"]:active,button:active{background:#88c}
+#main_nav{background:#1d7548}
+#titlebox{margin-bottom:0}
+.outerbox{background:#3b6b94}
+.innerbox{margin:0;padding:10px}
+.links{background:#5b8bb4}
+#threadlist{background:#154a72;border:1px inset #1b3345}
+.mainpage .thread{padding:5px}
+#thread_nav{background:#1d7548;padding:6px}
+.name{color:#afd775}
+.name em{color:#009}
+.del a{color:#fff}
+.msg{margin:4px 40px 24px}
+.q{color:#aac}
+.yt{background:#2f587a;border:1px solid #1b2933}
+.msg hr{display:block}
+.deleted{color:#afafaf;margin-bottom:24px}
+.abbrev{color:#bbb}
+.size{color:#de9d7f}
+.postform,#createbox,.threads .outerbox,#content{border-right:2px solid #5b8bb4;border-bottom:2px solid #5b8bb4;background:#3b6b94}
+.postform{padding:10px}
+.threadpage .threadlinks{margin:0 0 3px}
+form .msg{border:1px dashed #000;background:#1b3345}
+#q-p{background:#1b3345;border:1px solid #3b6b94}
+#q-p .msg,#q-p a.thumb{margin-bottom:8px}
+.lastposts{background:#5b8bb4;padding:8px}
+#content{padding:0 10px 10px}
+#content.list #header div{background:#3b6b94}
+#content.list .row:nth-child(odd){background:#1b3345}
+#content.list .row:nth-child(even){background:#294b68}
+#content.grid{border:0;padding:0}
+#content.grid .row{border-width:0 2px 2px 0;border-style:solid;border-color:#5b8bb4}
+#content.grid .row:hover{background:#154a72}
+@media(max-width:720px){
+ .mainpage .thread{padding:0}
+ .innerbox,.postform,.mainpage .postform{padding:8px;padding-left:8px}
+} \ No newline at end of file
diff --git a/static/css/txt/headline.css b/static/css/txt/headline.css
new file mode 100644
index 0000000..8fb085b
--- /dev/null
+++ b/static/css/txt/headline.css
@@ -0,0 +1,41 @@
+body,#main_nav{background:#FFF}
+body,textarea{color:#000}
+a,#n2,h3 span{color:#D60}
+a:active .name,h2 a:active,h4 a:active,a:active,#n2:active{color:#FA4}
+h2 a,.del a,.name,h4 a,h4{color:#000}
+h1{border-bottom:2px solid #F70;padding:0 10px;margin:0 -10px 10px}
+h2,h3{background:#EEE;border-bottom:2px solid #F70;display:inline-block;padding:2px 3px}
+h3{margin:0.7em 0}
+h4{background:#EEE;border-bottom:1px solid #BBB;padding:0 3px}
+hr{display:none}
+input,input[type="text"],textarea,button{border:1px solid #000}
+input[type="submit"],input[type="button"],button{background:#DDD;padding:2px 10px}
+input[type="submit"]:active,input[type="button"]:active,button:active{background:#CCC}
+.outerbox,#titlebox .innerbox:first-child,.postform,.lastposts,#content{background:#EEE;border-bottom:2px solid #BBB}
+.innerbox{margin:0;padding:10px}
+#threadlist{background:#fff;border:1px inset #bbb}
+.mainpage .thread{padding:5px}
+#thread_nav{border-bottom:2px solid #F70;padding:0 0 6px 6px}
+.mainpage .first div.msg::first-letter{font-size:1.5em;text-transform:uppercase}
+.name em{color:#009;font-style:normal}
+.q,.abbrev{color:#7f7f7f}
+.yt{background:#f2f2f2;border:1px solid #bbb}
+.msg hr{display:block;max-width:500px;margin-left:0}
+.deleted{color:#AFAFAF}
+#q-p{background:#FFF;border:1px solid #BBB;border-bottom:2px solid #BBB}
+.lastposts{padding:8px}
+.size{color:#D60}
+.threadpage .threadlinks{margin:0 0 3px}
+.postform{padding:10px}
+form .msg{border:1px dashed #000;background:#FFF;margin:0;padding:.5em 0.15em}
+#content{padding:0 10px 10px}
+#content.list #header div{background:#EEE}
+#content.list .row:nth-child(odd){background:#FFF}
+#content.list .row:nth-child(even){background:#DDD}
+#content.grid{border:1px solid #BBB;padding:0}
+#content.grid .row{border:1px solid #BBB}
+#content.grid .row:hover{background:#DDD}
+@media(max-width:720px){
+ .mainpage .thread{padding:0}
+ .innerbox,.postform,.mainpage .postform{padding:8px;padding-left:8px}
+} \ No newline at end of file
diff --git a/static/css/txt/postal.css b/static/css/txt/postal.css
new file mode 100644
index 0000000..d91b523
--- /dev/null
+++ b/static/css/txt/postal.css
@@ -0,0 +1,47 @@
+body,textarea{color:#000}
+body{background:#E5B98D}
+a,a .name,#n2{color:#3683C2}
+a:active,a:active .name,#n2:active{color:#E9976B}
+input[type="text"],textarea{border:1px solid #CCC}
+input[type="submit"],input[type="button"],button{background:#CBCBCB;border:0;padding:3px 10px}
+input[type="submit"]:active,input[type="button"]:active,button:active{background:#B1B1B1}
+body>hr{position:absolute;top:0;left:8px;right:8px;border:0;background-image:repeating-linear-gradient(125deg,#EE4C47 0px,#EE4C47 30px,#FFF 30px,#FFF 50px,#5CACEB 50px,#5CACEB 80px,#FFF 80px,#FFF 100px);height:1em}
+hr,.threadpage .thread{background-image:repeating-linear-gradient(125deg,#EE4C47 0px,#EE4C47 30px,#FFF 30px,#FFF 50px,#5CACEB 50px,#5CACEB 80px,#FFF 80px,#FFF 100px)}
+h2 span{color:#686868}
+h2 a,h2 a:active,h3{color:#3E3020}
+h2 a:active{text-decoration:underline}
+h3{margin-top:0}
+h3 span{color:dimgrey}
+h4{background:#F0E7CE;color:#686868;padding-left:3px}
+#main_nav,.outerbox{background:#FFF}
+.outerbox{border:1px solid #FFF}
+#threadlist{background:#EFEFEF;border:1px inset #EFEFEF}
+.thread .innerbox{background:#FFF;box-shadow:1px 1px 0 rgba(0,0,0,0.1),3px 3px 0 rgba(255,255,255,1),4px 4px 0 rgba(0,0,0,0.125),6px 6px 0 rgba(255,255,255,1),7px 7px 0 rgba(0,0,0,0.15),9px 9px 0 rgba(255,255,255,1),10px 10px 0 rgba(0,0,0,0.175),12px 12px 0 rgba(255,255,255,1),13px 13px 0 rgba(0,0,0,0.175);margin:8px 12px 8px 0;padding:8px}
+#thread_nav{background:#FFF;font-style:italic;padding:1.5em 8px .5em}
+.threadpage .thread{background-color:#FFF;background-position:bottom;background-repeat:repeat-x;background-size:100% 1em;overflow:hidden;padding:0 10px 1.5em}
+.innerbox.links {border-top:2px solid #E5B98D;margin:-1px;padding:12px}
+.name,.abbrev{color:#416D91}
+.name em{color:#009}
+.del a{color:#000}
+.q{color:#666;display:inline-block}
+.q:hover{background:#F0F0E0}
+.yt{background:#EEE;border:1px solid #CCC}
+.deleted{background:##F7F3E6;color:#AAA}
+#q-p{background:#FFF;border:1px solid #CCC}
+#createbox{background:#F0E7CE;border-color:#F0E7CE;color:#353535}
+.lastposts,.threadpage .postform{margin-top:.5em;padding:.5em}
+.lastposts,.threadpage .postform,.threads .outerbox,#content.list,#content.grid .row{background:#FFF;-webkit-box-shadow:0 0 10px rgba(0,0,0,0.3);-moz-box-shadow:0 0 10px rgba(0,0,0,0.3);box-shadow:0 0 10px rgba(0,0,0,0.3)}
+.threadpage .threadlinks{margin-top:0}
+.size{margin-bottom:0}
+form .msg{border:1px dotted #CCC;background:#FFF}
+#footer{color:#333}
+#content.list{padding:8px}
+#content.list #header div{background:#6AA9D7}
+#content.list .row:hover,#content.grid .row:hover{background:#B7D2E8}
+#content.list .row a:hover,#content.grid .row:hover a{color:#0801BF}
+#content.grid .row{margin:4px}
+@media(max-width:720px){
+ body.threadpage{margin:0 0 8px}
+ body>hr{top:-8px;left:0;right:0}
+ #createbox .innerbox{padding:0}
+} \ No newline at end of file
diff --git a/static/css/txt/sjis.css b/static/css/txt/sjis.css
new file mode 100644
index 0000000..018c6db
--- /dev/null
+++ b/static/css/txt/sjis.css
@@ -0,0 +1 @@
+body,textarea{font-family:Mona,Monapo,IPAMonaPGothic,'MS PGothic',YOzFontAA97}textarea{font-size:16px}
diff --git a/static/css/txt/ventanas.css b/static/css/txt/ventanas.css
new file mode 100644
index 0000000..cddee34
--- /dev/null
+++ b/static/css/txt/ventanas.css
@@ -0,0 +1,48 @@
+body,textarea{color:#000}
+body.mainpage,body.threads{background:#008081}
+body.threadpage,.outerbox,.mainpage .thread,#q-p{background:#C0C0C0}
+::selection{background:blue;color:white}::-moz-selection{background:blue;color:white}
+a,a .name,#n2{color:#00F}
+a:active,a:active .name,#n2:active{color:#f00}
+input[type=submit],input[type=button],button{border-width:2px;border-color:#FFF #000 #000 #FFF;border-style:ridge;background:#C0C0C0;color:#000;padding:2px 10px}
+h1,h2,#threadlinks:before,h5{background:#010081;color:#FFF;font-size:21px;font-weight:bold;margin:-11px -11px 11px;padding:2px}
+h2 a,h2 a:active{color:#FFF}
+h3 span{color:#555;text-decoration:underline}
+hr{border:0}
+hr,.reply,.deleted{border-bottom:2px groove white}
+#main_nav{background:#C0C0C0;border-bottom:2px ridge white;line-height:1;padding:2px 0}
+.threadnav{line-height:1;margin:-11px}
+#main_nav a,.threadnav a{background:#C0C0C0;border-width:2px;border-color:#FFF #000 #000 #FFF;border-style:ridge;color:#000;display:inline-block;margin:0 -1px;padding:2px;text-decoration:none;outline-offset:-4px}
+.threadnav a{margin:2px;padding:0}
+#main_nav a:first-child{font-weight:bold}
+#main_nav .cur_brd{background:#BBB url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAHUlEQVQImQESAO3/AMG/wv////7/AP///v++vr7/gXcOcY36q6gAAAAASUVORK5CYII=');font-weight:bold}
+#main_nav .cur_brd,#main_nav a:active,.threadnav a:active,input[type=submit]:active,input[type=button]:active,button:active{border-color:#000 #FFF #FFF #000}
+.outerbox,.mainpage .thread,#q-p{border-width:2px;border-color:#FFF #000 #000 #FFF;border-style:groove}
+input[type=text],textarea,.innerbox,#content,#threadlist{border-width:2px;border-color:#000 #FFF #FFF #000;border-style:groove}
+#threadlinks:before{content:"";display:block;height:15px;margin-bottom:5px}
+.innerbox{padding:10px;margin:2px}
+.links{border:1px solid;border-color:#888 #dadada #dadada #888;padding:2px}
+#threadlist{background:#FFF}
+#threadlist a{color:#000}
+#thread_nav a{color:#000;display:inline-block;margin:-3px -2px;padding:3px}#thread_nav a:hover{background:#DDD}
+.reply,.deleted{margin-bottom:.5em}
+.name,.abbrev{color:#444}
+.name em{color:#009}
+.del a{color:#000}
+.q{color:#666}
+.yt{background:#FFF;border:1px inset #aaa;color:#000}
+.deleted{color:gray;padding-bottom:.5em}
+#q-p{box-shadow:5px 5px #000;padding:0}
+#q-p .reply{padding:.5em;margin-bottom:0}
+#q-p a.thumb,#q-p .msg{margin-bottom:.5em}
+.postform,.formpad{padding:0}
+form .msg{background:#FFFFE0;border:1px solid #000}
+#footer{color:#FFF}
+#listmenu h1{background:none;color:#000;margin:0;padding:0}
+#content{background:#FFF}
+#content.list #header div{background:#CCC}
+#content.list .row div,#content.grid .row{border-width:0 1px 1px 0;border-style:solid;border-color:#000}
+#content .row:hover{background:#EEE}
+@media(max-width:720px){
+ #createbox form{margin:-10px}
+} \ No newline at end of file
diff --git a/static/css/vndb.css b/static/css/vndb.css
new file mode 100644
index 0000000..43a9c7c
--- /dev/null
+++ b/static/css/vndb.css
@@ -0,0 +1,30 @@
+html,body{background-attachment:scroll,fixed;background-color:#000;background-image:url('img/vndb1.jpg'),url('img/vndb2.jpg');background-position:left top,right top;background-repeat:no-repeat,no-repeat;color:#DDD;font-family:Arial,Helvetica,"Nimbus Sans L",sans-serif}
+.rep{color:#DDD}
+a,a .name,.nav label{color:#77BBDD}
+a:hover,a:hover .name,.nav label:hover{border-bottom:1px dotted #DDD;text-decoration:none}
+#catalog .thread > a:hover{border-bottom:none;text-decoration:underline}
+.logo{color:#135;font-style:italic}
+.replymode,.extramode{color:#FFF}
+.replymode{background:#258}
+.extramode{background:#247}
+.postblock{border:1px solid #258;background:rgba(7,28,47,0.75)}
+input,input[type="text"],input[type="password"],textarea{background:#0d2741;border:1px solid #35A;color:#DDD}
+input[type="submit"]{background:rgba(7,28,47,0.9);padding:3px 10px}
+input[type="submit"]:active{background:#0d2741}
+.fs{text-decoration:none}
+.subj{color:#7bd;font-weight:bold}
+.ell,.name,.omitted{color:#258}
+.q{border-left:1px dotted #258;color:#336da7}
+.reply,#q-p{background:rgba(7,28,47,0.8);border:1px solid #258}
+.abbrev{color:#707070}
+.highlight{background:rgb(10,35,60)}
+.managertable{border:1px solid #258}
+.managertable td{background:rgba(7,28,47,0.5)}
+.managertable th{background:rgba(7,28,47,0.9)}
+hr{border:none;border-top:1px solid #258;height:0}
+#catalog .thread:hover{background:rgba(7,28,47,0.92);box-shadow:0 0 5px 5px rgba(7,28,47,0.9)}
+#catalog .replies{color:#258;font-weight:bold}
+.yt,.yt:hover{background:#0d2741;border:1px solid #258;color:#DDD}
+.pg,.pg td{background:rgba(7,28,47,0.8);border:1px solid #258}
+.footer,.footer a{color:#247}
+.quoted{border-color:#35A;color:#247} \ No newline at end of file
diff --git a/static/ico/1372836.gif b/static/ico/1372836.gif
new file mode 100644
index 0000000..9ebe25b
--- /dev/null
+++ b/static/ico/1372836.gif
Binary files differ
diff --git a/static/ico/1k.gif b/static/ico/1k.gif
new file mode 100644
index 0000000..95847ec
--- /dev/null
+++ b/static/ico/1k.gif
Binary files differ
diff --git a/static/ico/2-1.gif b/static/ico/2-1.gif
new file mode 100644
index 0000000..a7f2b98
--- /dev/null
+++ b/static/ico/2-1.gif
Binary files differ
diff --git a/static/ico/2ppa.gif b/static/ico/2ppa.gif
new file mode 100644
index 0000000..23088ea
--- /dev/null
+++ b/static/ico/2ppa.gif
Binary files differ
diff --git a/static/ico/2syobo_2.gif b/static/ico/2syobo_2.gif
new file mode 100644
index 0000000..d54d613
--- /dev/null
+++ b/static/ico/2syobo_2.gif
Binary files differ
diff --git a/static/ico/3-2.gif b/static/ico/3-2.gif
new file mode 100644
index 0000000..c0aff5a
--- /dev/null
+++ b/static/ico/3-2.gif
Binary files differ
diff --git a/static/ico/3.gif b/static/ico/3.gif
new file mode 100644
index 0000000..fb65b6d
--- /dev/null
+++ b/static/ico/3.gif
Binary files differ
diff --git a/static/ico/3na.gif b/static/ico/3na.gif
new file mode 100644
index 0000000..2c30e0c
--- /dev/null
+++ b/static/ico/3na.gif
Binary files differ
diff --git a/static/ico/4-2.gif b/static/ico/4-2.gif
new file mode 100644
index 0000000..0d43898
--- /dev/null
+++ b/static/ico/4-2.gif
Binary files differ
diff --git a/static/ico/4248688.gif b/static/ico/4248688.gif
new file mode 100644
index 0000000..ef629f0
--- /dev/null
+++ b/static/ico/4248688.gif
Binary files differ
diff --git a/static/ico/5007629.gif b/static/ico/5007629.gif
new file mode 100644
index 0000000..9fa6a22
--- /dev/null
+++ b/static/ico/5007629.gif
Binary files differ
diff --git a/static/ico/5296219.gif b/static/ico/5296219.gif
new file mode 100644
index 0000000..a77351d
--- /dev/null
+++ b/static/ico/5296219.gif
Binary files differ
diff --git a/static/ico/5ta.gif b/static/ico/5ta.gif
new file mode 100644
index 0000000..04800b1
--- /dev/null
+++ b/static/ico/5ta.gif
Binary files differ
diff --git a/static/ico/6396408.gif b/static/ico/6396408.gif
new file mode 100644
index 0000000..ba33fc2
--- /dev/null
+++ b/static/ico/6396408.gif
Binary files differ
diff --git a/static/ico/6za.gif b/static/ico/6za.gif
new file mode 100644
index 0000000..bd4a945
--- /dev/null
+++ b/static/ico/6za.gif
Binary files differ
diff --git a/static/ico/8028885.gif b/static/ico/8028885.gif
new file mode 100644
index 0000000..cca1680
--- /dev/null
+++ b/static/ico/8028885.gif
Binary files differ
diff --git a/static/ico/8toushinnomonar16.gif b/static/ico/8toushinnomonar16.gif
new file mode 100644
index 0000000..e4ac044
--- /dev/null
+++ b/static/ico/8toushinnomonar16.gif
Binary files differ
diff --git a/static/ico/8toushinnomonar32.gif b/static/ico/8toushinnomonar32.gif
new file mode 100644
index 0000000..4c31836
--- /dev/null
+++ b/static/ico/8toushinnomonar32.gif
Binary files differ
diff --git a/static/ico/ace.gif b/static/ico/ace.gif
new file mode 100644
index 0000000..f521c7e
--- /dev/null
+++ b/static/ico/ace.gif
Binary files differ
diff --git a/static/ico/af1.gif b/static/ico/af1.gif
new file mode 100644
index 0000000..e88e02b
--- /dev/null
+++ b/static/ico/af1.gif
Binary files differ
diff --git a/static/ico/af2.gif b/static/ico/af2.gif
new file mode 100644
index 0000000..fe46aa6
--- /dev/null
+++ b/static/ico/af2.gif
Binary files differ
diff --git a/static/ico/ahya_xmas_2.gif b/static/ico/ahya_xmas_2.gif
new file mode 100644
index 0000000..6c0aebc
--- /dev/null
+++ b/static/ico/ahya_xmas_2.gif
Binary files differ
diff --git a/static/ico/aka.gif b/static/ico/aka.gif
new file mode 100644
index 0000000..c22596b
--- /dev/null
+++ b/static/ico/aka.gif
Binary files differ
diff --git a/static/ico/ame.gif b/static/ico/ame.gif
new file mode 100644
index 0000000..08dca67
--- /dev/null
+++ b/static/ico/ame.gif
Binary files differ
diff --git a/static/ico/anime_buun02.gif b/static/ico/anime_buun02.gif
new file mode 100644
index 0000000..c3422f2
--- /dev/null
+++ b/static/ico/anime_buun02.gif
Binary files differ
diff --git a/static/ico/anime_charhan01.gif b/static/ico/anime_charhan01.gif
new file mode 100644
index 0000000..cb04d65
--- /dev/null
+++ b/static/ico/anime_charhan01.gif
Binary files differ
diff --git a/static/ico/anime_charhan02.gif b/static/ico/anime_charhan02.gif
new file mode 100644
index 0000000..92eb054
--- /dev/null
+++ b/static/ico/anime_charhan02.gif
Binary files differ
diff --git a/static/ico/anime_giko01.gif b/static/ico/anime_giko01.gif
new file mode 100644
index 0000000..d4a6f84
--- /dev/null
+++ b/static/ico/anime_giko01.gif
Binary files differ
diff --git a/static/ico/anime_giko04.gif b/static/ico/anime_giko04.gif
new file mode 100644
index 0000000..bab9e77
--- /dev/null
+++ b/static/ico/anime_giko04.gif
Binary files differ
diff --git a/static/ico/anime_giko10.gif b/static/ico/anime_giko10.gif
new file mode 100644
index 0000000..c9db6cc
--- /dev/null
+++ b/static/ico/anime_giko10.gif
Binary files differ
diff --git a/static/ico/anime_giko11.gif b/static/ico/anime_giko11.gif
new file mode 100644
index 0000000..80af838
--- /dev/null
+++ b/static/ico/anime_giko11.gif
Binary files differ
diff --git a/static/ico/anime_giko12.gif b/static/ico/anime_giko12.gif
new file mode 100644
index 0000000..201d60c
--- /dev/null
+++ b/static/ico/anime_giko12.gif
Binary files differ
diff --git a/static/ico/anime_giko13.gif b/static/ico/anime_giko13.gif
new file mode 100644
index 0000000..446bed8
--- /dev/null
+++ b/static/ico/anime_giko13.gif
Binary files differ
diff --git a/static/ico/anime_hossyu01.gif b/static/ico/anime_hossyu01.gif
new file mode 100644
index 0000000..66be979
--- /dev/null
+++ b/static/ico/anime_hossyu01.gif
Binary files differ
diff --git a/static/ico/anime_imanouchi01.gif b/static/ico/anime_imanouchi01.gif
new file mode 100644
index 0000000..d3eacf5
--- /dev/null
+++ b/static/ico/anime_imanouchi01.gif
Binary files differ
diff --git a/static/ico/anime_iyou02.gif b/static/ico/anime_iyou02.gif
new file mode 100644
index 0000000..1f3216c
--- /dev/null
+++ b/static/ico/anime_iyou02.gif
Binary files differ
diff --git a/static/ico/anime_jien01.gif b/static/ico/anime_jien01.gif
new file mode 100644
index 0000000..bd2d8ed
--- /dev/null
+++ b/static/ico/anime_jien01.gif
Binary files differ
diff --git a/static/ico/anime_jien02.gif b/static/ico/anime_jien02.gif
new file mode 100644
index 0000000..51d5393
--- /dev/null
+++ b/static/ico/anime_jien02.gif
Binary files differ
diff --git a/static/ico/anime_jien03.gif b/static/ico/anime_jien03.gif
new file mode 100644
index 0000000..2e59a55
--- /dev/null
+++ b/static/ico/anime_jien03.gif
Binary files differ
diff --git a/static/ico/anime_jyorujyu01.gif b/static/ico/anime_jyorujyu01.gif
new file mode 100644
index 0000000..6e3bca8
--- /dev/null
+++ b/static/ico/anime_jyorujyu01.gif
Binary files differ
diff --git a/static/ico/anime_jyorujyu02.gif b/static/ico/anime_jyorujyu02.gif
new file mode 100644
index 0000000..5d13a89
--- /dev/null
+++ b/static/ico/anime_jyorujyu02.gif
Binary files differ
diff --git a/static/ico/anime_jyorujyu03.gif b/static/ico/anime_jyorujyu03.gif
new file mode 100644
index 0000000..5034286
--- /dev/null
+++ b/static/ico/anime_jyorujyu03.gif
Binary files differ
diff --git a/static/ico/anime_kukkuru01.gif b/static/ico/anime_kukkuru01.gif
new file mode 100644
index 0000000..7a817cd
--- /dev/null
+++ b/static/ico/anime_kukkuru01.gif
Binary files differ
diff --git a/static/ico/anime_kuma01.gif b/static/ico/anime_kuma01.gif
new file mode 100644
index 0000000..5c1cbae
--- /dev/null
+++ b/static/ico/anime_kuma01.gif
Binary files differ
diff --git a/static/ico/anime_kumaface01.gif b/static/ico/anime_kumaface01.gif
new file mode 100644
index 0000000..e6027fc
--- /dev/null
+++ b/static/ico/anime_kumaface01.gif
Binary files differ
diff --git a/static/ico/anime_loop.gif b/static/ico/anime_loop.gif
new file mode 100644
index 0000000..19f2e9b
--- /dev/null
+++ b/static/ico/anime_loop.gif
Binary files differ
diff --git a/static/ico/anime_marara02.gif b/static/ico/anime_marara02.gif
new file mode 100644
index 0000000..7d3fb7f
--- /dev/null
+++ b/static/ico/anime_marara02.gif
Binary files differ
diff --git a/static/ico/anime_matanki01.gif b/static/ico/anime_matanki01.gif
new file mode 100644
index 0000000..7ab5b77
--- /dev/null
+++ b/static/ico/anime_matanki01.gif
Binary files differ
diff --git a/static/ico/anime_matanki02.gif b/static/ico/anime_matanki02.gif
new file mode 100644
index 0000000..342eca3
--- /dev/null
+++ b/static/ico/anime_matanki02.gif
Binary files differ
diff --git a/static/ico/anime_miruna01.gif b/static/ico/anime_miruna01.gif
new file mode 100644
index 0000000..85e1c5d
--- /dev/null
+++ b/static/ico/anime_miruna01.gif
Binary files differ
diff --git a/static/ico/anime_monar02.gif b/static/ico/anime_monar02.gif
new file mode 100644
index 0000000..c8bfdfc
--- /dev/null
+++ b/static/ico/anime_monar02.gif
Binary files differ
diff --git a/static/ico/anime_monar03.gif b/static/ico/anime_monar03.gif
new file mode 100644
index 0000000..b3e061c
--- /dev/null
+++ b/static/ico/anime_monar03.gif
Binary files differ
diff --git a/static/ico/anime_monar05.gif b/static/ico/anime_monar05.gif
new file mode 100644
index 0000000..4d934fb
--- /dev/null
+++ b/static/ico/anime_monar05.gif
Binary files differ
diff --git a/static/ico/anime_morara01.gif b/static/ico/anime_morara01.gif
new file mode 100644
index 0000000..e9d4808
--- /dev/null
+++ b/static/ico/anime_morara01.gif
Binary files differ
diff --git a/static/ico/anime_morara02.gif b/static/ico/anime_morara02.gif
new file mode 100644
index 0000000..0cf4f97
--- /dev/null
+++ b/static/ico/anime_morara02.gif
Binary files differ
diff --git a/static/ico/anime_morara04.gif b/static/ico/anime_morara04.gif
new file mode 100644
index 0000000..846d642
--- /dev/null
+++ b/static/ico/anime_morara04.gif
Binary files differ
diff --git a/static/ico/anime_nokar01.gif b/static/ico/anime_nokar01.gif
new file mode 100644
index 0000000..89ea5bb
--- /dev/null
+++ b/static/ico/anime_nokar01.gif
Binary files differ
diff --git a/static/ico/anime_okashi01.gif b/static/ico/anime_okashi01.gif
new file mode 100644
index 0000000..5e4e518
--- /dev/null
+++ b/static/ico/anime_okashi01.gif
Binary files differ
diff --git a/static/ico/anime_okashi02.gif b/static/ico/anime_okashi02.gif
new file mode 100644
index 0000000..6ba6ec3
--- /dev/null
+++ b/static/ico/anime_okashi02.gif
Binary files differ
diff --git a/static/ico/anime_onigiri04.gif b/static/ico/anime_onigiri04.gif
new file mode 100644
index 0000000..78f3f66
--- /dev/null
+++ b/static/ico/anime_onigiri04.gif
Binary files differ
diff --git a/static/ico/anime_saitama01.gif b/static/ico/anime_saitama01.gif
new file mode 100644
index 0000000..b36dcb5
--- /dev/null
+++ b/static/ico/anime_saitama01.gif
Binary files differ
diff --git a/static/ico/anime_saitama02.gif b/static/ico/anime_saitama02.gif
new file mode 100644
index 0000000..0a81ff2
--- /dev/null
+++ b/static/ico/anime_saitama02.gif
Binary files differ
diff --git a/static/ico/anime_saitama03.gif b/static/ico/anime_saitama03.gif
new file mode 100644
index 0000000..5cf168b
--- /dev/null
+++ b/static/ico/anime_saitama03.gif
Binary files differ
diff --git a/static/ico/anime_sasuga01.gif b/static/ico/anime_sasuga01.gif
new file mode 100644
index 0000000..279d97a
--- /dev/null
+++ b/static/ico/anime_sasuga01.gif
Binary files differ
diff --git a/static/ico/anime_sasuga03.gif b/static/ico/anime_sasuga03.gif
new file mode 100644
index 0000000..047e2b9
--- /dev/null
+++ b/static/ico/anime_sasuga03.gif
Binary files differ
diff --git a/static/ico/anime_sasuga04.gif b/static/ico/anime_sasuga04.gif
new file mode 100644
index 0000000..0e9f839
--- /dev/null
+++ b/static/ico/anime_sasuga04.gif
Binary files differ
diff --git a/static/ico/anime_shii01.gif b/static/ico/anime_shii01.gif
new file mode 100644
index 0000000..99cf151
--- /dev/null
+++ b/static/ico/anime_shii01.gif
Binary files differ
diff --git a/static/ico/anime_shii02.gif b/static/ico/anime_shii02.gif
new file mode 100644
index 0000000..233869b
--- /dev/null
+++ b/static/ico/anime_shii02.gif
Binary files differ
diff --git a/static/ico/anime_shii03.gif b/static/ico/anime_shii03.gif
new file mode 100644
index 0000000..51f3723
--- /dev/null
+++ b/static/ico/anime_shii03.gif
Binary files differ
diff --git a/static/ico/anime_syobon01.gif b/static/ico/anime_syobon01.gif
new file mode 100644
index 0000000..bcbd7eb
--- /dev/null
+++ b/static/ico/anime_syobon01.gif
Binary files differ
diff --git a/static/ico/anime_syobon03.gif b/static/ico/anime_syobon03.gif
new file mode 100644
index 0000000..02bc032
--- /dev/null
+++ b/static/ico/anime_syobon03.gif
Binary files differ
diff --git a/static/ico/anime_tarn01.gif b/static/ico/anime_tarn01.gif
new file mode 100644
index 0000000..0de9031
--- /dev/null
+++ b/static/ico/anime_tarn01.gif
Binary files differ
diff --git a/static/ico/anime_uwan01.gif b/static/ico/anime_uwan01.gif
new file mode 100644
index 0000000..8945d5a
--- /dev/null
+++ b/static/ico/anime_uwan01.gif
Binary files differ
diff --git a/static/ico/anime_uwan02.gif b/static/ico/anime_uwan02.gif
new file mode 100644
index 0000000..c4e5c2d
--- /dev/null
+++ b/static/ico/anime_uwan02.gif
Binary files differ
diff --git a/static/ico/anime_uwan03.gif b/static/ico/anime_uwan03.gif
new file mode 100644
index 0000000..7ec0b3e
--- /dev/null
+++ b/static/ico/anime_uwan03.gif
Binary files differ
diff --git a/static/ico/anime_youkanman01.gif b/static/ico/anime_youkanman01.gif
new file mode 100644
index 0000000..8706427
--- /dev/null
+++ b/static/ico/anime_youkanman01.gif
Binary files differ
diff --git a/static/ico/anime_youkanman02.gif b/static/ico/anime_youkanman02.gif
new file mode 100644
index 0000000..6c3862a
--- /dev/null
+++ b/static/ico/anime_youkanman02.gif
Binary files differ
diff --git a/static/ico/anime_youkanman03.gif b/static/ico/anime_youkanman03.gif
new file mode 100644
index 0000000..a5973f6
--- /dev/null
+++ b/static/ico/anime_youkanman03.gif
Binary files differ
diff --git a/static/ico/anime_zonu01.gif b/static/ico/anime_zonu01.gif
new file mode 100644
index 0000000..964ed8c
--- /dev/null
+++ b/static/ico/anime_zonu01.gif
Binary files differ
diff --git a/static/ico/anime_zonu02.gif b/static/ico/anime_zonu02.gif
new file mode 100644
index 0000000..70d4572
--- /dev/null
+++ b/static/ico/anime_zonu02.gif
Binary files differ
diff --git a/static/ico/aramaki.gif b/static/ico/aramaki.gif
new file mode 100644
index 0000000..b3f6420
--- /dev/null
+++ b/static/ico/aramaki.gif
Binary files differ
diff --git a/static/ico/aroeri-na32.gif b/static/ico/aroeri-na32.gif
new file mode 100644
index 0000000..e6cf7d6
--- /dev/null
+++ b/static/ico/aroeri-na32.gif
Binary files differ
diff --git a/static/ico/asopasomaso.gif b/static/ico/asopasomaso.gif
new file mode 100644
index 0000000..2b153e4
--- /dev/null
+++ b/static/ico/asopasomaso.gif
Binary files differ
diff --git a/static/ico/bikyakusan32.gif b/static/ico/bikyakusan32.gif
new file mode 100644
index 0000000..62f8483
--- /dev/null
+++ b/static/ico/bikyakusan32.gif
Binary files differ
diff --git a/static/ico/bs.gif b/static/ico/bs.gif
new file mode 100644
index 0000000..25c1f0f
--- /dev/null
+++ b/static/ico/bs.gif
Binary files differ
diff --git a/static/ico/button1_03.gif b/static/ico/button1_03.gif
new file mode 100644
index 0000000..1e29394
--- /dev/null
+++ b/static/ico/button1_03.gif
Binary files differ
diff --git a/static/ico/buun.gif b/static/ico/buun.gif
new file mode 100644
index 0000000..81f9b62
--- /dev/null
+++ b/static/ico/buun.gif
Binary files differ
diff --git a/static/ico/chahan.gif b/static/ico/chahan.gif
new file mode 100644
index 0000000..d10116c
--- /dev/null
+++ b/static/ico/chahan.gif
Binary files differ
diff --git a/static/ico/dokuo1.gif b/static/ico/dokuo1.gif
new file mode 100644
index 0000000..70e9a1f
--- /dev/null
+++ b/static/ico/dokuo1.gif
Binary files differ
diff --git a/static/ico/file2_01.gif b/static/ico/file2_01.gif
new file mode 100644
index 0000000..9c88ab0
--- /dev/null
+++ b/static/ico/file2_01.gif
Binary files differ
diff --git a/static/ico/fujisan.gif b/static/ico/fujisan.gif
new file mode 100644
index 0000000..9e33757
--- /dev/null
+++ b/static/ico/fujisan.gif
Binary files differ
diff --git a/static/ico/fuun.gif b/static/ico/fuun.gif
new file mode 100644
index 0000000..7576689
--- /dev/null
+++ b/static/ico/fuun.gif
Binary files differ
diff --git a/static/ico/gaku.gif b/static/ico/gaku.gif
new file mode 100644
index 0000000..667c680
--- /dev/null
+++ b/static/ico/gaku.gif
Binary files differ
diff --git a/static/ico/gaku2.gif b/static/ico/gaku2.gif
new file mode 100644
index 0000000..6f30a26
--- /dev/null
+++ b/static/ico/gaku2.gif
Binary files differ
diff --git a/static/ico/gaku3.gif b/static/ico/gaku3.gif
new file mode 100644
index 0000000..685a0fc
--- /dev/null
+++ b/static/ico/gaku3.gif
Binary files differ
diff --git a/static/ico/gekisya1.gif b/static/ico/gekisya1.gif
new file mode 100644
index 0000000..7b2238f
--- /dev/null
+++ b/static/ico/gekisya1.gif
Binary files differ
diff --git a/static/ico/giko1.gif b/static/ico/giko1.gif
new file mode 100644
index 0000000..330a682
--- /dev/null
+++ b/static/ico/giko1.gif
Binary files differ
diff --git a/static/ico/gikog_gomibako.gif b/static/ico/gikog_gomibako.gif
new file mode 100644
index 0000000..cfdd338
--- /dev/null
+++ b/static/ico/gikog_gomibako.gif
Binary files differ
diff --git a/static/ico/gikog_gyunyupack.gif b/static/ico/gikog_gyunyupack.gif
new file mode 100644
index 0000000..8e2de6b
--- /dev/null
+++ b/static/ico/gikog_gyunyupack.gif
Binary files differ
diff --git a/static/ico/gikog_pimiento.gif b/static/ico/gikog_pimiento.gif
new file mode 100644
index 0000000..21e5eca
--- /dev/null
+++ b/static/ico/gikog_pimiento.gif
Binary files differ
diff --git a/static/ico/gikoinu.gif b/static/ico/gikoinu.gif
new file mode 100644
index 0000000..d923982
--- /dev/null
+++ b/static/ico/gikoinu.gif
Binary files differ
diff --git a/static/ico/gikoneko.gif b/static/ico/gikoneko.gif
new file mode 100644
index 0000000..f9e4999
--- /dev/null
+++ b/static/ico/gikoneko.gif
Binary files differ
diff --git a/static/ico/gikoneko2.gif b/static/ico/gikoneko2.gif
new file mode 100644
index 0000000..54d4647
--- /dev/null
+++ b/static/ico/gikoneko2.gif
Binary files differ
diff --git a/static/ico/gikoneko_1.gif b/static/ico/gikoneko_1.gif
new file mode 100644
index 0000000..a168dbf
--- /dev/null
+++ b/static/ico/gikoneko_1.gif
Binary files differ
diff --git a/static/ico/gocchin_face.gif b/static/ico/gocchin_face.gif
new file mode 100644
index 0000000..cfde101
--- /dev/null
+++ b/static/ico/gocchin_face.gif
Binary files differ
diff --git a/static/ico/gomiopen.gif b/static/ico/gomiopen.gif
new file mode 100644
index 0000000..56d9ada
--- /dev/null
+++ b/static/ico/gomiopen.gif
Binary files differ
diff --git a/static/ico/goo_1.gif b/static/ico/goo_1.gif
new file mode 100644
index 0000000..e3c2d93
--- /dev/null
+++ b/static/ico/goo_1.gif
Binary files differ
diff --git a/static/ico/goo_3.gif b/static/ico/goo_3.gif
new file mode 100644
index 0000000..b47825b
--- /dev/null
+++ b/static/ico/goo_3.gif
Binary files differ
diff --git a/static/ico/gya-.gif b/static/ico/gya-.gif
new file mode 100644
index 0000000..dfb2bd1
--- /dev/null
+++ b/static/ico/gya-.gif
Binary files differ
diff --git a/static/ico/hagenin-shuriken.gif b/static/ico/hagenin-shuriken.gif
new file mode 100644
index 0000000..51e3f8e
--- /dev/null
+++ b/static/ico/hagenin-shuriken.gif
Binary files differ
diff --git a/static/ico/hagurumaou.gif b/static/ico/hagurumaou.gif
new file mode 100644
index 0000000..06a1969
--- /dev/null
+++ b/static/ico/hagurumaou.gif
Binary files differ
diff --git a/static/ico/hikky.gif b/static/ico/hikky.gif
new file mode 100644
index 0000000..87e90e7
--- /dev/null
+++ b/static/ico/hikky.gif
Binary files differ
diff --git a/static/ico/hikky_xmas_2.gif b/static/ico/hikky_xmas_2.gif
new file mode 100644
index 0000000..162b96f
--- /dev/null
+++ b/static/ico/hikky_xmas_2.gif
Binary files differ
diff --git a/static/ico/hyou.gif b/static/ico/hyou.gif
new file mode 100644
index 0000000..0574ff5
--- /dev/null
+++ b/static/ico/hyou.gif
Binary files differ
diff --git a/static/ico/iirasan_face.gif b/static/ico/iirasan_face.gif
new file mode 100644
index 0000000..83b2462
--- /dev/null
+++ b/static/ico/iirasan_face.gif
Binary files differ
diff --git a/static/ico/imanouchi_1.gif b/static/ico/imanouchi_1.gif
new file mode 100644
index 0000000..9e242a0
--- /dev/null
+++ b/static/ico/imanouchi_1.gif
Binary files differ
diff --git a/static/ico/iyahoo.gif b/static/ico/iyahoo.gif
new file mode 100644
index 0000000..3387787
--- /dev/null
+++ b/static/ico/iyahoo.gif
Binary files differ
diff --git a/static/ico/iyou.gif b/static/ico/iyou.gif
new file mode 100644
index 0000000..bc94072
--- /dev/null
+++ b/static/ico/iyou.gif
Binary files differ
diff --git a/static/ico/jisakujien_2.gif b/static/ico/jisakujien_2.gif
new file mode 100644
index 0000000..0cc759a
--- /dev/null
+++ b/static/ico/jisakujien_2.gif
Binary files differ
diff --git a/static/ico/jisakujien_xmas.gif b/static/ico/jisakujien_xmas.gif
new file mode 100644
index 0000000..9c1c8c8
--- /dev/null
+++ b/static/ico/jisakujien_xmas.gif
Binary files differ
diff --git a/static/ico/kantoku1.gif b/static/ico/kantoku1.gif
new file mode 100644
index 0000000..6459dc4
--- /dev/null
+++ b/static/ico/kantoku1.gif
Binary files differ
diff --git a/static/ico/kappappa1.gif b/static/ico/kappappa1.gif
new file mode 100644
index 0000000..e182e84
--- /dev/null
+++ b/static/ico/kappappa1.gif
Binary files differ
diff --git a/static/ico/kasa-ri.gif b/static/ico/kasa-ri.gif
new file mode 100644
index 0000000..26fde75
--- /dev/null
+++ b/static/ico/kasa-ri.gif
Binary files differ
diff --git a/static/ico/kashiwamo-chi32.gif b/static/ico/kashiwamo-chi32.gif
new file mode 100644
index 0000000..5d99a57
--- /dev/null
+++ b/static/ico/kashiwamo-chi32.gif
Binary files differ
diff --git a/static/ico/kinokorusensei32.gif b/static/ico/kinokorusensei32.gif
new file mode 100644
index 0000000..e315517
--- /dev/null
+++ b/static/ico/kinokorusensei32.gif
Binary files differ
diff --git a/static/ico/kita_.gif b/static/ico/kita_.gif
new file mode 100644
index 0000000..b953217
--- /dev/null
+++ b/static/ico/kita_.gif
Binary files differ
diff --git a/static/ico/kodomona.gif b/static/ico/kodomona.gif
new file mode 100644
index 0000000..9998fb8
--- /dev/null
+++ b/static/ico/kodomona.gif
Binary files differ
diff --git a/static/ico/konkon_folder.gif b/static/ico/konkon_folder.gif
new file mode 100644
index 0000000..7a1f3c6
--- /dev/null
+++ b/static/ico/konkon_folder.gif
Binary files differ
diff --git a/static/ico/kossorisan.gif b/static/ico/kossorisan.gif
new file mode 100644
index 0000000..96e177c
--- /dev/null
+++ b/static/ico/kossorisan.gif
Binary files differ
diff --git a/static/ico/kotatu.gif b/static/ico/kotatu.gif
new file mode 100644
index 0000000..2b0fa89
--- /dev/null
+++ b/static/ico/kotatu.gif
Binary files differ
diff --git a/static/ico/kuma.gif b/static/ico/kuma.gif
new file mode 100644
index 0000000..453e2c3
--- /dev/null
+++ b/static/ico/kuma.gif
Binary files differ
diff --git a/static/ico/kuma2.gif b/static/ico/kuma2.gif
new file mode 100644
index 0000000..a3e7109
--- /dev/null
+++ b/static/ico/kuma2.gif
Binary files differ
diff --git a/static/ico/maimai.gif b/static/ico/maimai.gif
new file mode 100644
index 0000000..0a37a4f
--- /dev/null
+++ b/static/ico/maimai.gif
Binary files differ
diff --git a/static/ico/makotan2_folder.gif b/static/ico/makotan2_folder.gif
new file mode 100644
index 0000000..ea6996c
--- /dev/null
+++ b/static/ico/makotan2_folder.gif
Binary files differ
diff --git a/static/ico/mona.gif b/static/ico/mona.gif
new file mode 100644
index 0000000..2163e99
--- /dev/null
+++ b/static/ico/mona.gif
Binary files differ
diff --git a/static/ico/mona_shiri.gif b/static/ico/mona_shiri.gif
new file mode 100644
index 0000000..f1c209a
--- /dev/null
+++ b/static/ico/mona_shiri.gif
Binary files differ
diff --git a/static/ico/mona_tya.gif b/static/ico/mona_tya.gif
new file mode 100644
index 0000000..ae5feb6
--- /dev/null
+++ b/static/ico/mona_tya.gif
Binary files differ
diff --git a/static/ico/monaazarashi_1.gif b/static/ico/monaazarashi_1.gif
new file mode 100644
index 0000000..b6c09ca
--- /dev/null
+++ b/static/ico/monaazarashi_1.gif
Binary files differ
diff --git a/static/ico/namaetukenai.gif b/static/ico/namaetukenai.gif
new file mode 100644
index 0000000..a381eba
--- /dev/null
+++ b/static/ico/namaetukenai.gif
Binary files differ
diff --git a/static/ico/naoruyo.gif b/static/ico/naoruyo.gif
new file mode 100644
index 0000000..0dc9c83
--- /dev/null
+++ b/static/ico/naoruyo.gif
Binary files differ
diff --git a/static/ico/nida.gif b/static/ico/nida.gif
new file mode 100644
index 0000000..8c383b6
--- /dev/null
+++ b/static/ico/nida.gif
Binary files differ
diff --git a/static/ico/nigete.gif b/static/ico/nigete.gif
new file mode 100644
index 0000000..271dfb1
--- /dev/null
+++ b/static/ico/nigete.gif
Binary files differ
diff --git a/static/ico/nono_ie.gif b/static/ico/nono_ie.gif
new file mode 100644
index 0000000..302971b
--- /dev/null
+++ b/static/ico/nono_ie.gif
Binary files differ
diff --git a/static/ico/nurupo_ga_2.gif b/static/ico/nurupo_ga_2.gif
new file mode 100644
index 0000000..4cc8611
--- /dev/null
+++ b/static/ico/nurupo_ga_2.gif
Binary files differ
diff --git a/static/ico/onigiri_seito.gif b/static/ico/onigiri_seito.gif
new file mode 100644
index 0000000..9be7160
--- /dev/null
+++ b/static/ico/onigiri_seito.gif
Binary files differ
diff --git a/static/ico/otiketu48.gif b/static/ico/otiketu48.gif
new file mode 100644
index 0000000..3787378
--- /dev/null
+++ b/static/ico/otiketu48.gif
Binary files differ
diff --git a/static/ico/pc3.gif b/static/ico/pc3.gif
new file mode 100644
index 0000000..a4ddc7d
--- /dev/null
+++ b/static/ico/pc3.gif
Binary files differ
diff --git a/static/ico/pgya.gif b/static/ico/pgya.gif
new file mode 100644
index 0000000..edfad21
--- /dev/null
+++ b/static/ico/pgya.gif
Binary files differ
diff --git a/static/ico/sasuga1.gif b/static/ico/sasuga1.gif
new file mode 100644
index 0000000..1f8bfd9
--- /dev/null
+++ b/static/ico/sasuga1.gif
Binary files differ
diff --git a/static/ico/seito_2.gif b/static/ico/seito_2.gif
new file mode 100644
index 0000000..3ff3e3c
--- /dev/null
+++ b/static/ico/seito_2.gif
Binary files differ
diff --git a/static/ico/soon.gif b/static/ico/soon.gif
new file mode 100644
index 0000000..95e4d74
--- /dev/null
+++ b/static/ico/soon.gif
Binary files differ
diff --git a/static/ico/tasukete.gif b/static/ico/tasukete.gif
new file mode 100644
index 0000000..9e4fe3a
--- /dev/null
+++ b/static/ico/tasukete.gif
Binary files differ
diff --git a/static/ico/torimasu1.gif b/static/ico/torimasu1.gif
new file mode 100644
index 0000000..64fd5fe
--- /dev/null
+++ b/static/ico/torimasu1.gif
Binary files differ
diff --git a/static/ico/torimasu2.gif b/static/ico/torimasu2.gif
new file mode 100644
index 0000000..e3ec154
--- /dev/null
+++ b/static/ico/torimasu2.gif
Binary files differ
diff --git a/static/ico/u_ame.gif b/static/ico/u_ame.gif
new file mode 100644
index 0000000..1369356
--- /dev/null
+++ b/static/ico/u_ame.gif
Binary files differ
diff --git a/static/ico/u_hoshi.gif b/static/ico/u_hoshi.gif
new file mode 100644
index 0000000..6b8adbd
--- /dev/null
+++ b/static/ico/u_hoshi.gif
Binary files differ
diff --git a/static/ico/u_naoruyo_bath.gif b/static/ico/u_naoruyo_bath.gif
new file mode 100644
index 0000000..def8ca6
--- /dev/null
+++ b/static/ico/u_naoruyo_bath.gif
Binary files differ
diff --git a/static/ico/u_okotowari_a.gif b/static/ico/u_okotowari_a.gif
new file mode 100644
index 0000000..ed43164
--- /dev/null
+++ b/static/ico/u_okotowari_a.gif
Binary files differ
diff --git a/static/ico/u_sofa.gif b/static/ico/u_sofa.gif
new file mode 100644
index 0000000..783aa98
--- /dev/null
+++ b/static/ico/u_sofa.gif
Binary files differ
diff --git a/static/ico/wakannai1.gif b/static/ico/wakannai1.gif
new file mode 100644
index 0000000..eeecb33
--- /dev/null
+++ b/static/ico/wakannai1.gif
Binary files differ
diff --git a/static/ico/yakimochi.gif b/static/ico/yakimochi.gif
new file mode 100644
index 0000000..8652360
--- /dev/null
+++ b/static/ico/yakimochi.gif
Binary files differ
diff --git a/static/ico/youkan.gif b/static/ico/youkan.gif
new file mode 100644
index 0000000..d1c386e
--- /dev/null
+++ b/static/ico/youkan.gif
Binary files differ
diff --git a/static/ico/zonu_1.gif b/static/ico/zonu_1.gif
new file mode 100644
index 0000000..816c451
--- /dev/null
+++ b/static/ico/zonu_1.gif
Binary files differ
diff --git a/static/ico/zuzagiko48.gif b/static/ico/zuzagiko48.gif
new file mode 100644
index 0000000..c4dd620
--- /dev/null
+++ b/static/ico/zuzagiko48.gif
Binary files differ
diff --git a/static/img/anarkia.jpg b/static/img/anarkia.jpg
new file mode 100644
index 0000000..43db329
--- /dev/null
+++ b/static/img/anarkia.jpg
Binary files differ
diff --git a/static/img/bai.jpg b/static/img/bai.jpg
new file mode 100644
index 0000000..ac41b4d
--- /dev/null
+++ b/static/img/bai.jpg
Binary files differ
diff --git a/static/img/cero.gif b/static/img/cero.gif
new file mode 100644
index 0000000..9ab0f0f
--- /dev/null
+++ b/static/img/cero.gif
Binary files differ
diff --git a/static/img/default.png b/static/img/default.png
new file mode 100644
index 0000000..9cd9b93
--- /dev/null
+++ b/static/img/default.png
Binary files differ
diff --git a/static/img/juegos1.jpg b/static/img/juegos1.jpg
new file mode 100644
index 0000000..b908128
--- /dev/null
+++ b/static/img/juegos1.jpg
Binary files differ
diff --git a/static/img/juegos2.jpg b/static/img/juegos2.jpg
new file mode 100644
index 0000000..0cc9cc5
--- /dev/null
+++ b/static/img/juegos2.jpg
Binary files differ
diff --git a/static/img/juegos3.png b/static/img/juegos3.png
new file mode 100644
index 0000000..685a3d0
--- /dev/null
+++ b/static/img/juegos3.png
Binary files differ
diff --git a/static/img/juegos4.gif b/static/img/juegos4.gif
new file mode 100644
index 0000000..40aca15
--- /dev/null
+++ b/static/img/juegos4.gif
Binary files differ
diff --git a/static/img/letras1.png b/static/img/letras1.png
new file mode 100644
index 0000000..719de0d
--- /dev/null
+++ b/static/img/letras1.png
Binary files differ
diff --git a/static/img/letras2.png b/static/img/letras2.png
new file mode 100644
index 0000000..61affa4
--- /dev/null
+++ b/static/img/letras2.png
Binary files differ
diff --git a/static/img/letras3.png b/static/img/letras3.png
new file mode 100644
index 0000000..78183be
--- /dev/null
+++ b/static/img/letras3.png
Binary files differ
diff --git a/static/img/letras4.jpg b/static/img/letras4.jpg
new file mode 100644
index 0000000..32c5ff6
--- /dev/null
+++ b/static/img/letras4.jpg
Binary files differ
diff --git a/static/img/letras5.jpg b/static/img/letras5.jpg
new file mode 100644
index 0000000..61128bc
--- /dev/null
+++ b/static/img/letras5.jpg
Binary files differ
diff --git a/static/img/musica1.jpg b/static/img/musica1.jpg
new file mode 100644
index 0000000..3cd141d
--- /dev/null
+++ b/static/img/musica1.jpg
Binary files differ
diff --git a/static/img/noticias.png b/static/img/noticias.png
new file mode 100644
index 0000000..1ee2187
--- /dev/null
+++ b/static/img/noticias.png
Binary files differ
diff --git a/static/img/old/2d_1.jpg b/static/img/old/2d_1.jpg
new file mode 100644
index 0000000..01216b3
--- /dev/null
+++ b/static/img/old/2d_1.jpg
Binary files differ
diff --git a/static/img/old/2d_2.jpg b/static/img/old/2d_2.jpg
new file mode 100644
index 0000000..dd9c234
--- /dev/null
+++ b/static/img/old/2d_2.jpg
Binary files differ
diff --git a/static/img/old/2d_3.png b/static/img/old/2d_3.png
new file mode 100644
index 0000000..034cf1e
--- /dev/null
+++ b/static/img/old/2d_3.png
Binary files differ
diff --git a/static/img/old/2d_4.jpg b/static/img/old/2d_4.jpg
new file mode 100644
index 0000000..7f5c7fe
--- /dev/null
+++ b/static/img/old/2d_4.jpg
Binary files differ
diff --git a/static/img/old/argentina1.png b/static/img/old/argentina1.png
new file mode 100644
index 0000000..4dc22b6
--- /dev/null
+++ b/static/img/old/argentina1.png
Binary files differ
diff --git a/static/img/old/chile1.png b/static/img/old/chile1.png
new file mode 100644
index 0000000..37bda0b
--- /dev/null
+++ b/static/img/old/chile1.png
Binary files differ
diff --git a/static/img/old/chile2.jpg b/static/img/old/chile2.jpg
new file mode 100644
index 0000000..558212a
--- /dev/null
+++ b/static/img/old/chile2.jpg
Binary files differ
diff --git a/static/img/old/g0.jpg b/static/img/old/g0.jpg
new file mode 100644
index 0000000..b70c449
--- /dev/null
+++ b/static/img/old/g0.jpg
Binary files differ
diff --git a/static/img/old/g1.jpg b/static/img/old/g1.jpg
new file mode 100644
index 0000000..30315c9
--- /dev/null
+++ b/static/img/old/g1.jpg
Binary files differ
diff --git a/static/img/old/g2.jpg b/static/img/old/g2.jpg
new file mode 100644
index 0000000..c073028
--- /dev/null
+++ b/static/img/old/g2.jpg
Binary files differ
diff --git a/static/img/old/g3.jpg b/static/img/old/g3.jpg
new file mode 100644
index 0000000..9686553
--- /dev/null
+++ b/static/img/old/g3.jpg
Binary files differ
diff --git a/static/img/old/g4.jpg b/static/img/old/g4.jpg
new file mode 100644
index 0000000..f49404b
--- /dev/null
+++ b/static/img/old/g4.jpg
Binary files differ
diff --git a/static/img/old/g5.jpg b/static/img/old/g5.jpg
new file mode 100644
index 0000000..959aaaf
--- /dev/null
+++ b/static/img/old/g5.jpg
Binary files differ
diff --git a/static/img/old/peli.jpg b/static/img/old/peli.jpg
new file mode 100644
index 0000000..0b953b9
--- /dev/null
+++ b/static/img/old/peli.jpg
Binary files differ
diff --git a/static/img/old/salon2d_3.jpg b/static/img/old/salon2d_3.jpg
new file mode 100644
index 0000000..592b04f
--- /dev/null
+++ b/static/img/old/salon2d_3.jpg
Binary files differ
diff --git a/static/img/old/salon2d_4.png b/static/img/old/salon2d_4.png
new file mode 100644
index 0000000..bbcd904
--- /dev/null
+++ b/static/img/old/salon2d_4.png
Binary files differ
diff --git a/static/img/old/salon2d_5.jpg b/static/img/old/salon2d_5.jpg
new file mode 100644
index 0000000..35151b1
--- /dev/null
+++ b/static/img/old/salon2d_5.jpg
Binary files differ
diff --git a/static/img/old/zine.png b/static/img/old/zine.png
new file mode 100644
index 0000000..2812b34
--- /dev/null
+++ b/static/img/old/zine.png
Binary files differ
diff --git a/static/img/salon2d_1.png b/static/img/salon2d_1.png
new file mode 100644
index 0000000..7295a44
--- /dev/null
+++ b/static/img/salon2d_1.png
Binary files differ
diff --git a/static/img/salon2d_3.jpg b/static/img/salon2d_3.jpg
new file mode 100644
index 0000000..592b04f
--- /dev/null
+++ b/static/img/salon2d_3.jpg
Binary files differ
diff --git a/static/img/salon2d_4.png b/static/img/salon2d_4.png
new file mode 100644
index 0000000..bbcd904
--- /dev/null
+++ b/static/img/salon2d_4.png
Binary files differ
diff --git a/static/img/salon2d_5.jpg b/static/img/salon2d_5.jpg
new file mode 100644
index 0000000..35151b1
--- /dev/null
+++ b/static/img/salon2d_5.jpg
Binary files differ
diff --git a/static/img/tech1.png b/static/img/tech1.png
new file mode 100644
index 0000000..3e851c2
--- /dev/null
+++ b/static/img/tech1.png
Binary files differ
diff --git a/static/img/tech2.jpg b/static/img/tech2.jpg
new file mode 100644
index 0000000..cd982ff
--- /dev/null
+++ b/static/img/tech2.jpg
Binary files differ
diff --git a/static/img/tech3.png b/static/img/tech3.png
new file mode 100644
index 0000000..b7ad99d
--- /dev/null
+++ b/static/img/tech3.png
Binary files differ
diff --git a/static/img/tech4.jpg b/static/img/tech4.jpg
new file mode 100644
index 0000000..d9d959d
--- /dev/null
+++ b/static/img/tech4.jpg
Binary files differ
diff --git a/static/img/tech5.jpg b/static/img/tech5.jpg
new file mode 100644
index 0000000..b19df49
--- /dev/null
+++ b/static/img/tech5.jpg
Binary files differ
diff --git a/static/img/tech6.png b/static/img/tech6.png
new file mode 100644
index 0000000..32faa9b
--- /dev/null
+++ b/static/img/tech6.png
Binary files differ
diff --git a/static/img/tv1.png b/static/img/tv1.png
new file mode 100644
index 0000000..17ab84b
--- /dev/null
+++ b/static/img/tv1.png
Binary files differ
diff --git a/static/img/weird-al.jpg b/static/img/weird-al.jpg
new file mode 100644
index 0000000..10de1c6
--- /dev/null
+++ b/static/img/weird-al.jpg
Binary files differ
diff --git a/static/img/world.gif b/static/img/world.gif
new file mode 100644
index 0000000..f6a8d80
--- /dev/null
+++ b/static/img/world.gif
Binary files differ
diff --git a/static/img/zonavip1.jpg b/static/img/zonavip1.jpg
new file mode 100644
index 0000000..2d8d98f
--- /dev/null
+++ b/static/img/zonavip1.jpg
Binary files differ
diff --git a/static/img/zonavip2.gif b/static/img/zonavip2.gif
new file mode 100644
index 0000000..0e37bae
--- /dev/null
+++ b/static/img/zonavip2.gif
Binary files differ
diff --git a/static/img/zonavip3.png b/static/img/zonavip3.png
new file mode 100644
index 0000000..323e346
--- /dev/null
+++ b/static/img/zonavip3.png
Binary files differ
diff --git a/static/img/zonavip4.jpg b/static/img/zonavip4.jpg
new file mode 100644
index 0000000..98d0ef4
--- /dev/null
+++ b/static/img/zonavip4.jpg
Binary files differ
diff --git a/static/img/zonavip5.gif b/static/img/zonavip5.gif
new file mode 100644
index 0000000..dc69964
--- /dev/null
+++ b/static/img/zonavip5.gif
Binary files differ
diff --git a/static/img/zonavip6.png b/static/img/zonavip6.png
new file mode 100644
index 0000000..aa3443b
--- /dev/null
+++ b/static/img/zonavip6.png
Binary files differ
diff --git a/static/img/zonavip7.gif b/static/img/zonavip7.gif
new file mode 100644
index 0000000..ff96166
--- /dev/null
+++ b/static/img/zonavip7.gif
Binary files differ
diff --git a/static/img/zonavip8.png b/static/img/zonavip8.png
new file mode 100644
index 0000000..847ad9a
--- /dev/null
+++ b/static/img/zonavip8.png
Binary files differ
diff --git a/static/img/zonavip9.gif b/static/img/zonavip9.gif
new file mode 100644
index 0000000..ba636df
--- /dev/null
+++ b/static/img/zonavip9.gif
Binary files differ
diff --git a/static/img/zonavip9.jpg b/static/img/zonavip9.jpg
new file mode 100644
index 0000000..9590b48
--- /dev/null
+++ b/static/img/zonavip9.jpg
Binary files differ
diff --git a/static/img/zonavip_halloween.jpg b/static/img/zonavip_halloween.jpg
new file mode 100644
index 0000000..674792f
--- /dev/null
+++ b/static/img/zonavip_halloween.jpg
Binary files differ
diff --git a/static/img/zonavip_nav.jpg b/static/img/zonavip_nav.jpg
new file mode 100644
index 0000000..506abf9
--- /dev/null
+++ b/static/img/zonavip_nav.jpg
Binary files differ
diff --git a/static/js/aquiencitas.js b/static/js/aquiencitas.js
new file mode 100644
index 0000000..0cf860f
--- /dev/null
+++ b/static/js/aquiencitas.js
@@ -0,0 +1,168 @@
+var cur_url;
+var linklist;
+var linki;
+var is_bbs;
+var plimit = 5;
+function getPostRange(t, n) {
+ var posts, replies, s, ss, ee, rev = false;
+ posts = [];
+ replies = t.getElementsByClassName("reply");
+ s = n.split('-');
+ ss = parseInt(s[0]);
+ ee = ss;
+ if(s.length == 2) ee = parseInt(s[1]);
+ if(ee<ss) { tmp=ss;ss=ee;ee=tmp; rev=true; }
+ for(j = 0; j < replies.length; j++) {
+ num = parseInt(replies[j].dataset.n);
+ if(num > ee) break;
+ if(num >= ss && num <= ee) {
+ if(rev) posts.unshift(replies[j]);
+ else posts.push(replies[j]);
+ }
+ }
+ return posts;
+}
+function findAncestor (el) {
+ while ((el = el.parentElement) && !el.className.startsWith("thread") && !el.className.startsWith("cont"));
+ return el;
+}
+function getPostDivs(e) {
+ if(is_bbs) {
+ divs = [];
+ t = findAncestor(e);
+ s = e.getAttribute('href').split('/');
+ r = s[s.length-1];
+ rs = r.split(',');
+ linki = 0;
+ for(i=0;i<rs.length;i++) { divs.push.apply(divs, getPostRange(t, rs[i])); }
+ return divs;
+ } else {
+ ele = document.getElementById('reply' + e.getAttribute('href').split('#')[1]);
+ return [ele,];
+ }
+}
+function get_pid(e) {
+ return is_bbs ? e.dataset.n : e.id.substr(5);
+}
+function fill_links(e) {
+ var divs = getPostDivs(e);
+ if(!divs[0]) return;
+
+ this_id = get_pid(e.parentNode.parentNode);
+
+ for(i=0;i<divs.length;i++) {
+ tid = get_pid(divs[i]);
+ if (linklist[tid])
+ continue;
+ if (this_id == tid)
+ continue;
+ t = (is_bbs ? divs[i].getElementsByTagName("h4")[0] : divs[i]);
+ bl = document.createElement('a');
+ bl.href = cur_url + (is_bbs ? "/" : "#") + this_id;
+ bl.textContent = '>>' + this_id;
+ bl.addEventListener('mouseover', who_are_you_quoting, false);
+ bl.addEventListener('mouseout', remove_quote_preview, false);
+ if (!(qb = t.getElementsByClassName('quoted')[0])) {
+ qb = document.createElement((is_bbs ? 'span' : 'div'));
+ qb.className = 'quoted';
+ qb.textContent = ' Citado por: ';
+ if(is_bbs) {
+ t.insertBefore(qb, t.getElementsByClassName("del")[0]);
+ t.insertBefore(document.createTextNode(' '), t.getElementsByClassName("del")[0]);
+ } else {
+ p = t.getElementsByTagName("blockquote");
+ p[p.length-1].insertAdjacentHTML('afterend', qb.outerHTML);
+ }
+ t.getElementsByClassName('quoted')[0].appendChild(bl);
+ } else {
+ qb.appendChild(document.createTextNode(' '));
+ qb.appendChild(bl);
+ }
+ linklist[tid] = true;
+ }
+}
+function who_are_you_quoting(e) {
+ var parent, d, clr, src, cnt, left, top, width, maxWidth;
+ e = e.target || window.event.srcElement;
+ var divs = getPostDivs(e);
+ if(!divs[0]) return;
+
+ maxWidth = 500;
+ cnt = document.createElement('div');
+ cnt.id = 'q-p';
+ width = divs[0].offsetWidth;
+ if (width > maxWidth) {
+ width = maxWidth;
+ }
+
+ for(i=0;i<divs.length&&i<plimit;i++) {
+ src = divs[i].cloneNode(true);
+ cnt.appendChild(src);
+ }
+ left = 0;
+ top = e.offsetHeight + 1;
+ parent = e;
+ do {
+ left += parent.offsetLeft;
+ top += parent.offsetTop;
+ } while (parent = parent.offsetParent);
+ if ((d = document.body.offsetWidth - left - width) < 0) left += d;
+ cnt.setAttribute('style', 'left:' + left + 'px;top:' + top + 'px;');
+ document.body.appendChild(cnt);
+}
+function remove_quote_preview(e) {
+ var cnt;
+ if (cnt = document.getElementById('q-p'))
+ document.body.removeChild(cnt);
+}
+/*function goTo(e) {
+ e.preventDefault();
+ var pst = this.textContent.split(/[-,]/)[0];
+ pst = "r"+ pst.slice(2);
+ pst = document.getElementById(pst);
+ if (pst) pst.scrollIntoView();
+}*/
+function quotePreview() {
+ if(localStorage.getItem("shobon_on") == "false") { return; }
+ if(localStorage.getItem("shobon_preview") == "false" && localStorage.getItem("shobon_backlink") == "false") { return; }
+
+ var i, q, replies, quotes;
+
+ if(document.body.className && document.body.className != "res")
+ is_bbs = true;
+ else is_bbs = false;
+
+ if(is_bbs) replies = document.getElementsByClassName('msg');
+ else replies = document.getElementsByTagName('blockquote');
+
+ urls = window.location.pathname.split("/");
+ cur_url = urls[0] + "/" + urls[1] + "/" + urls[2] + "/" + urls[3];
+
+ for (x = 0; x < replies.length; x++) {
+ quotes = replies[x].getElementsByTagName('a');
+ linklist = {};
+
+ for (i = 0; i < quotes.length; i++) {
+ q = quotes[i];
+ if(q.textContent.length < 3 || !q.textContent.startsWith(">>")) continue;
+
+ if(localStorage.getItem("shobon_preview") != "false") {
+ q.addEventListener('mouseover', who_are_you_quoting, false);
+ q.addEventListener('mouseout', remove_quote_preview, false);
+ }
+
+ if(localStorage.getItem("shobon_backlink") != "false") {
+ fill_links(q);
+ }
+ }
+ }
+/* if (document.body.className === "threadpage") {
+ for (x = 0; x < replies.length; x++) {
+ var q = replies[x].getElementsByTagName("a");
+ for(var j=0;j<q.length;j++) {
+ if(q[j].textContent.startsWith(">>")) q[j].addEventListener("click", goTo, false);
+ }
+ }
+ }*/
+}
+document.addEventListener('DOMContentLoaded', quotePreview, false); \ No newline at end of file
diff --git a/static/js/autorefresh.js b/static/js/autorefresh.js
new file mode 100644
index 0000000..0ed3e06
--- /dev/null
+++ b/static/js/autorefresh.js
@@ -0,0 +1,275 @@
+var lastTime = 0;
+var refreshInterval;
+var refreshMaxTime = 30;
+var refreshTime;
+var manual = 0;
+var serviceType = 0; // 2 = BBS, 3 = IB
+var thread_length = 0;
+var thread_lastreply = 0;
+var thread_title = "";
+var thread_first_length = 0;
+var http_request = new XMLHttpRequest();
+
+function checkNew(e) {
+ e.preventDefault();
+ manual = 1;
+ loadJSON();
+ if (chk.checked) refreshMaxTime = 25;
+}
+
+function loadJSON() {
+ if (chk.checked)
+ stopCounter("...");
+ if (manual)
+ document.getElementById("counter").innerText = "...";
+ var data_file;
+ if (serviceType == 2 || serviceType == 3) {
+ board = document.getElementsByName("board")[0].value;
+ parent = document.getElementsByName("parent")[0].value;
+ data_file = "/cgi/api/thread?dir=" + board + "&id=" + parent + "&offset=" + thread_length + "&time=" + lastTime;
+ } else {
+ return false;
+ }
+ http_request.open("GET", data_file, true);
+ http_request.send();
+}
+
+function updateThread(posts, total_replies, serverTime) {
+ thread_div = document.getElementsByClassName("thread")[0];
+ if (serviceType == 2)
+ last_elem = document.getElementsByClassName("size")[0];
+ else
+ last_elem = document.getElementsByClassName("cut")[0];
+
+ for (var i = 0; i < posts.length; i++) {
+ post = posts[i];
+ var div = document.createElement('div');
+ if (serviceType == 2) div.className = "reply";
+ else div.className = "replycont";
+ if (post.email) {
+ if (post.tripcode) s_name = '<a href="mailto:' + post.email + '"><span class="name"><b>' + post.name + '</b> ' + post.tripcode + '</span></a>';
+ else s_name = '<a href="mailto:' + post.email + '"><span class="name"><b>' + post.name + '</b></span></a>';
+ } else {
+ if (post.tripcode) s_name = '<span class="name"><b>' + post.name + '</b> ' + post.tripcode + '</span>';
+ else s_name = '<span class="name"><b>' + post.name + '</b></span>';
+ }
+ if (serviceType == 2) {
+ if (post.file) {
+ s_img = '<a href="/' + board + '/src/' + post.file + '" target="_blank" class="thumb"><img src="/' + board + '/thumb/' + post.thumb + '" width="' + post.thumb_width + '" height="' + post.thumb_height + '" /><br />' + Math.round(post.file_size/1024) + 'KB ' + post.file.substring(post.file.lastIndexOf(".")+1, post.file.length).toUpperCase() + '</a>';
+ } else s_img = '';
+ if (post.IS_DELETED == 1) div.innerHTML = '<h4 class="deleted">' + (thread_length + i + 1) + ' : Mensaje eliminado por el usuario.</h4>';
+ else if (post.IS_DELETED == 2) div.innerHTML = '<h4 class="deleted">' + (thread_length + i + 1) + ' : Mensaje eliminado por miembro del staff.</h4>';
+ else
+ div.innerHTML = '<h4>' + (thread_length + i + 1) + ' : ' + s_name + ' : <span class="date" data-unix="' + post.timestamp + '">' + post.timestamp_formatted + '</span> <span class="del"><a href="/cgi/report/' + board + '/' + post.id + '/' + (thread_length + i + 1) + '" rel="nofollow">rep</a> <a href="#">del</a></span></h4>' + s_img + '<div class="msg">' + post.message + '</div>';
+ } else {
+ if (post.file) {
+ if (post.image_width != 0) {
+ s_img = '<div class="fs"><a href="/' + board + '/src/' + post.file + '" class="expimg" data-id="' + post.id + '" data-thumb="/' + board + '/thumb/' + post.thumb + '" data-w="' + post.image_width + '" data-h="' + post.image_height + '" data-tw="' + post.thumb_width + '" data-th="' + post.thumb_height + '">' + post.file + '</a>-(' + post.file_size+ ' B, ' + post.image_width + 'x' + post.image_height + ')</div>';
+ } else {
+ s_img = '<div class="fs"><a href="/' + board + '/src/' + post.file + '" target="_blank">' + post.file + '</a>-(' + post.file_size+ ' B)</div>';
+ }
+ s_img += '<a target="_blank" href="/' + board + '/src/' + post.file + '" id="thumb' + post.id + '"><img class="thumb" alt="' + post.id + '" src="/' + board + '/thumb/' + post.thumb + '" width="' + post.thumb_width + '" height="' + post.thumb_height + '" /></a>';
+ s_msg = '<blockquote style="margin-left:' + (post.thumb_width+40) + 'px;">' + post.message + '</blockquote>';
+ } else {
+ s_img = '';
+ s_msg = '<blockquote>' + post.message + '</blockquote>';
+ }
+ if (post.IS_DELETED == 0) {
+ div.innerHTML = '<table border="0"><tr><td class="ell">…</td><td class="reply" id="reply' + post.id + '"><div class="info"><input type="checkbox" name="delete" value="' + post.id + '" />' + (post.subject ? (' <span class="subj">' + post.subject + '</span>') : '') + ' ' + s_name + ' ' + '<span class="date" data-unix="' + post.timestamp + '">' + post.timestamp_formatted + '</span> <span class="reflink"><a href="#' + post.id + '">No.</a><a href="#" class="postid">' + post.id + '</a></span> <a class="rep" href="/cgi/report/' + board + '/' + post.id + '" rel="nofollow">rep</a></div>' + s_img + s_msg + '</td></tr></table>';
+ }
+ }
+
+ thread_div.insertBefore(div, last_elem);
+ thread_div.setAttribute("data-length",(thread_length + i + 1));
+ if (serviceType == 2)
+ document.getElementsByTagName("h3")[0].getElementsByTagName("span")[0].innerText = "(" + (thread_length + i + 1) + " respuestas)";
+ }
+
+ if (posts.length > 0) {
+ if (!manual)
+ refreshMaxTime = 10;
+ if (!document.hasFocus())
+ if (posts.length > 1)
+ notif(thread_title, posts.length + ' nuevos mensajes');
+ else
+ notif(thread_title, 'Un nuevo mensaje');
+ } else {
+ if (refreshMaxTime <= 60)
+ refreshMaxTime += 5;
+ }
+
+ thread_length = parseInt(total_replies) + 1;
+ //document.getElementsByClassName("thread")[0].firstChild.children[0].innerHTML = "("+thread_length+")";
+ new_unread = thread_length - thread_first_length;
+
+ if (new_unread)
+ document.title = "(" + new_unread + ") " + thread_title;
+ else
+ document.title = thread_title;
+}
+
+function notif(title, msg) {
+ var n = new Notification(title, {
+ body: msg
+ });
+ setTimeout(n.close.bind(n), 10000);
+}
+
+function counter() {
+ if (refreshTime < 1) {
+ loadJSON();
+ } else {
+ refreshTime--;
+ document.getElementById("counter").innerHTML = (refreshTime + 1);
+ }
+}
+
+function detectService() {
+ if (document.body.className === "threadpage") {
+ if (!document.getElementById("n")) return;
+ thread_title = document.title;
+ thread_length = parseInt(document.getElementsByClassName("thread")[0].dataset.length);
+ thread_first_length = thread_length;
+ replylist = document.getElementsByClassName("reply");
+ lastr = replylist[replylist.length - 1].textContent;
+ thread_lastreply = parseInt(lastr.substr(0, lastr.indexOf(" :")));
+ if (thread_length == thread_lastreply) {
+ serviceType = 2;
+ document.getElementById("n").addEventListener("click", checkNew);
+ var footer = document.getElementsByClassName("lastposts")[0];
+ var in1 = document.createElement("input");
+ in1.id = "autorefresh";
+ in1.setAttribute("type", "checkbox");
+ in1.addEventListener("click", autoRefresh);
+ in1.style.display = "none";
+ var in2 = document.createElement("label");
+ in2.id = "n2";
+ in2.setAttribute("for", "autorefresh");
+ in2.style.marginRight = "4px";
+ in2.style.cursor = "pointer";
+ in2.textContent = "Auto refresh";
+ var in3 = document.createElement("span");
+ in3.id = "counter";
+ in3.style.position = "absolute";
+ in3.textContent = "OFF";
+ footer.appendChild(document.createTextNode(" | "));
+ footer.appendChild(in1);
+ footer.appendChild(in2);
+ footer.appendChild(in3);
+ return true;
+ } else {
+ return false;
+ }
+ } else if (document.body.className === "res") {
+ serviceType = 3;
+ thread_title = document.title;
+ thread_length = parseInt(document.getElementsByClassName("thread")[0].dataset.length);
+ thread_first_length = thread_length;
+ replylist = document.getElementsByClassName("thread");
+ replylist += document.getElementsByClassName("reply");
+ var footer = document.getElementsByClassName("nav")[0];
+ var mnl = document.createElement("a");
+ mnl.id = "shownew";
+ mnl.href = "#";
+ mnl.textContent = "Ver nuevos posts";
+ var in1 = document.createElement("input");
+ in1.id = "autorefresh";
+ in1.setAttribute("type", "checkbox");
+ in1.addEventListener("click", autoRefresh);
+ in1.style.display = "none";
+ var in2 = document.createElement("label");
+ in2.setAttribute("for", "autorefresh");
+ in2.style.cursor = "pointer";
+ in2.title = "Ver nuevos posts automáticamente";
+ in2.textContent = "Auto";
+ var in4 = document.createElement("span");
+ in4.id = "counter";
+ in4.textContent = "OFF";
+ footer.appendChild(document.createTextNode(" ["));
+ footer.appendChild(mnl);
+ document.getElementById("shownew").addEventListener("click", checkNew);
+ footer.appendChild(document.createTextNode("] ["));
+ footer.appendChild(in1);
+ footer.appendChild(in2);
+ footer.appendChild(document.createTextNode("] "));
+ footer.appendChild(in4);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function startCounter() {
+ refreshTime = refreshMaxTime;
+ counter();
+ refreshInterval = setInterval(counter, 1000);
+}
+
+function stopCounter(str) {
+ clearInterval(refreshInterval);
+ document.getElementById("counter").innerHTML = str;
+}
+
+function autoRefresh(e) {
+ chk_snd = document.getElementById("autosound");
+ if (document.getElementById("autorefresh").checked) {
+ if (chk_snd)
+ chk_snd.disabled = false;
+ Notification.requestPermission();
+ lastTime = Math.floor(Date.now() / 1000);
+ refreshTime = refreshMaxTime;
+ startCounter();
+ } else {
+ if (chk_snd)
+ document.getElementById("autosound").disabled = true;
+ stopCounter("OFF");
+ }
+}
+
+http_request.onreadystatechange = function() {
+ if (http_request.readyState == 4) {
+ var jsonObj = JSON.parse(http_request.responseText);
+ if (jsonObj.state == "success") {
+ if (serviceType == 2 || serviceType == 3)
+ updateThread(jsonObj.posts, jsonObj.total_replies, jsonObj.time);
+ lastTime = jsonObj.time;
+ if (chk.checked)
+ startCounter();
+ }
+ if (!chk.checked) {
+ document.getElementById("counter").innerText = "OFF";
+ }
+ manual = 0;
+ }
+}
+document.addEventListener("DOMContentLoaded", function() {
+ if (!detectService()) return;
+
+ chk = document.getElementById("autorefresh");
+ chk_snd = document.getElementById("autosound");
+
+ if (localStorage.getItem("autorefresh")) {
+ document.getElementById("autorefresh").checked = true;
+ autoRefresh();
+ }
+ if (!chk_snd) return;
+ if (localStorage.getItem("mainpage_nosound"))
+ document.getElementById("autosound").checked = false;
+});
+
+window.addEventListener("unload", function() {
+ if (!serviceType) return;
+
+ chk = document.getElementById("autorefresh");
+ chk_snd = document.getElementById("autosound");
+
+ if (chk.checked)
+ localStorage.setItem("autorefresh", true);
+ else
+ localStorage.removeItem("autorefresh");
+ if (!chk_snd) return;
+ if (!document.getElementById("autosound").checked)
+ localStorage.setItem("mainpage_nosound", true);
+ else
+ localStorage.removeItem("mainpage_nosound");
+}); \ No newline at end of file
diff --git a/static/js/home.js b/static/js/home.js
new file mode 100644
index 0000000..0c921ec
--- /dev/null
+++ b/static/js/home.js
@@ -0,0 +1,173 @@
+console.log("%c¡Es calidad BaI!", "font-size: 50px; font-weight: bold;");
+
+function set_stylesheet(styletitle) {
+ opcs.style = styletitle;
+ parse();
+ var links=document.getElementsByTagName("link");
+ var found=false;
+ for(var i=0;i<links.length;i++) {
+ var rel=links[i].getAttribute("rel");
+ var title=links[i].getAttribute("title");
+ if(rel.indexOf("style")!=-1&&title) {
+ links[i].disabled=true; // IE needs this to work. IE needs to die.
+ if(styletitle==title) { links[i].disabled=false; found=true; }
+ }
+ }
+}
+
+function get_active_stylesheet() {
+ var links=document.getElementsByTagName("link");
+ for(var i=0;i<links.length;i++) {
+ var rel=links[i].getAttribute("rel");
+ var title=links[i].getAttribute("title");
+ if(rel.indexOf("style")!=-1&&title&&!links[i].disabled) return title;
+ }
+ return null;
+}
+
+function check_news() {
+ var last_t = opcs.last;
+ var items = document.getElementsByClassName('ni');
+ var dates = document.getElementsByClassName('ni-d');
+ for(var i=0; i<items.length; i++) if(parseInt(items[i].dataset.t) > last_t) {
+ items[i].className += ' urgent';
+ dates[i].innerHTML = '<img src="/new.gif" style="width:18px;height:7px;"><br />' + dates[i].innerHTML;
+ }
+ opcs.last = Date.now() / 1000 | 0;
+ parse();
+}
+
+var lastTime = 0;
+var refreshInterval;
+var refreshMaxTime = 30;
+var refreshTime;
+var unread = {};
+var last_threads = 0;
+var last_serverTime = 0;
+var http_request = new XMLHttpRequest();
+
+function loadJSON() {
+ stopCounter("...");
+ var data_file = "/cgi/api/lastage?time=" + lastTime + "&limit=" + document.getElementById("limit").value;
+ http_request.open("GET", data_file, true);
+ http_request.send();
+}
+
+function setRead(threadId) {
+ if (threadId in unread) {
+ unread[threadId] = false;
+ updatePostList(last_threads, last_serverTime);
+ }
+}
+
+function updatePostList(threads, serverTime) {
+ if (refreshMaxTime <= 120) refreshMaxTime += 5;
+ var arrayLength = threads.length;
+ if (!arrayLength) return;
+
+ html = "";
+ last_threads = threads;
+ last_serverTime = serverTime;
+
+ var newposts = 0;
+ var newTitle = "Bienvenido a Internet BBS/IB";
+ var new_unread = false;
+ var news = [];
+
+ for (var i = 0; i < arrayLength; i++) {
+ thread = threads[i];
+ if (thread.bumped >= lastTime) {
+ unread[thread.id] = true;
+ news.push('- ' + thread.board_fulln + ': ' + thread.content);
+ new_unread = true;
+ }
+ if (unread[thread.id]) html += '<span class="new">';
+ html += '<a href="' + thread.url + '" class="thread" data-brd="' + thread.board_fulln + '" data-unix="' + thread.timestamp + '" data-last="' + thread.bumped + '" data-img="' + thread.thumb + '"><span class="brd">[' + thread.board_name + ']</span> <span class="cont">' + thread.content + '</span> <span class="rep">(' + thread.length + ')</span></a>';
+ if (unread[thread.id]) {
+ html += '</span>';
+ newposts++;
+ }
+ }
+ if (newposts) newTitle = '(' + newposts + ') ' + newTitle;
+ if (new_unread) {
+ document.getElementById("newposts").style = "color:red";
+ notif('Bienvenido a Internet BBS/IB', 'Hay nuevos mensajes:\n' + news.join('\n'));
+ refreshMaxTime = 10;
+ if (document.getElementById('autosound').checked) {
+ document.getElementById("machina").volume = 0.6;
+ document.getElementById("machina").play();
+ }
+ }
+ window.parent.document.title = newTitle;
+ document.title = newTitle;
+ document.getElementById("postlist").innerHTML = html;
+}
+
+function notif(title, msg) {
+ var n = new Notification(title, { body: msg });
+ setTimeout(n.close.bind(n), 10000);
+}
+
+function counter() {
+ if (refreshTime < 1) loadJSON();
+ else {
+ refreshTime--;
+ document.getElementById("counter").innerHTML = "– " + (refreshTime + 1);
+ }
+}
+
+function startCounter() {
+ refreshTime = refreshMaxTime;
+ counter();
+ refreshInterval = setInterval(counter, 1000);
+}
+
+function stopCounter(str) {
+ clearInterval(refreshInterval);
+ document.getElementById("counter").innerHTML = str;
+}
+
+function autoRefresh(e) {
+ if (chk.checked) {
+ if (chk_snd) chk_snd.disabled = false;
+ Notification.requestPermission();
+ lastTime = Math.floor(Date.now() / 1000);
+ refreshTime = refreshMaxTime;
+ startCounter();
+ } else {
+ if (chk_snd) chk_snd.disabled = true;
+ stopCounter("");
+ }
+}
+
+http_request.onreadystatechange = function() {
+ if (http_request.readyState == 4) {
+ var jsonObj = JSON.parse(http_request.responseText);
+ if (jsonObj.state == "success") {
+ updatePostList(jsonObj.threads, jsonObj.time);
+ lastTime = jsonObj.time;
+ if (chk.checked) startCounter();
+ }
+ }
+}
+
+function parse() { localStorage.setItem("home", JSON.stringify(opcs)); }
+
+document.addEventListener("DOMContentLoaded", function() {
+ if (localStorage.hasOwnProperty("home")) opcs=JSON.parse(localStorage.getItem("home"));
+ else { opcs={"style":"IB","auto":false,"sound":false,"last":0}; parse(); }
+ set_stylesheet(opcs.style);
+
+ var css = document.getElementById("change_style").getElementsByTagName("a");
+ for(var j=0;j<css.length;j++) {
+ css[j].addEventListener("click", function(e) { e.preventDefault(); set_stylesheet(this.textContent); });
+ }
+ document.getElementById("autorefresh").addEventListener("click", function(e) { opcs.auto=!opcs.auto; autoRefresh(); parse(); });
+ document.getElementById("autosound").addEventListener("click", function(e) { opcs.sound=!opcs.sound; parse(); });
+ check_news();
+
+ chk=document.getElementById("autorefresh");
+ chk_snd=document.getElementById("autosound");
+ if (opcs.auto) { chk.checked=true; autoRefresh(); } else chk.checked=false;
+ if (opcs.sound) chk_snd.checked=true; else chk_snd.checked=false;
+}); \ No newline at end of file
diff --git a/static/js/jquery.js b/static/js/jquery.js
new file mode 100644
index 0000000..4d8cc18
--- /dev/null
+++ b/static/js/jquery.js
@@ -0,0 +1,545 @@
+(function(window,undefined){var rootjQuery,readyList,core_strundefined=typeof undefined,location=window.location,document=window.document,docElem=document.documentElement,_jQuery=window.jQuery,_$=window.$,class2type={},core_deletedIds=[],core_version="2.0.2",core_concat=core_deletedIds.concat,core_push=core_deletedIds.push,core_slice=core_deletedIds.slice,core_indexOf=core_deletedIds.indexOf,core_toString=class2type.toString,core_hasOwn=class2type.hasOwnProperty,core_trim=core_version.trim,jQuery=function(selector,context){return new jQuery.fn.init(selector,context,rootjQuery);},core_pnum=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,core_rnotwhite=/\S+/g,rquickExpr=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,rsingleTag=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,rmsPrefix=/^-ms-/,rdashAlpha=/-([\da-z])/gi,fcamelCase=function(all,letter){return letter.toUpperCase();},completed=function(){document.removeEventListener("DOMContentLoaded",completed,false);window.removeEventListener("load",completed,false);jQuery.ready();};jQuery.fn=jQuery.prototype={jquery:core_version,constructor:jQuery,init:function(selector,context,rootjQuery){var match,elem;if(!selector){return this;}
+if(typeof selector==="string"){if(selector.charAt(0)==="<"&&selector.charAt(selector.length-1)===">"&&selector.length>=3){match=[null,selector,null];}else{match=rquickExpr.exec(selector);}
+if(match&&(match[1]||!context)){if(match[1]){context=context instanceof jQuery?context[0]:context;jQuery.merge(this,jQuery.parseHTML(match[1],context&&context.nodeType?context.ownerDocument||context:document,true));if(rsingleTag.test(match[1])&&jQuery.isPlainObject(context)){for(match in context){if(jQuery.isFunction(this[match])){this[match](context[match]);}else{this.attr(match,context[match]);}}}
+return this;}else{elem=document.getElementById(match[2]);if(elem&&elem.parentNode){this.length=1;this[0]=elem;}
+this.context=document;this.selector=selector;return this;}}else{if(!context||context.jquery){return(context||rootjQuery).find(selector);}else{return this.constructor(context).find(selector);}}}else{if(selector.nodeType){this.context=this[0]=selector;this.length=1;return this;}else{if(jQuery.isFunction(selector)){return rootjQuery.ready(selector);}}}
+if(selector.selector!==undefined){this.selector=selector.selector;this.context=selector.context;}
+return jQuery.makeArray(selector,this);},selector:"",length:0,toArray:function(){return core_slice.call(this);},get:function(num){return num==null?this.toArray():num<0?this[this.length+num]:this[num];},pushStack:function(elems){var ret=jQuery.merge(this.constructor(),elems);ret.prevObject=this;ret.context=this.context;return ret;},each:function(callback,args){return jQuery.each(this,callback,args);},ready:function(fn){jQuery.ready.promise().done(fn);return this;},slice:function(){return this.pushStack(core_slice.apply(this,arguments));},first:function(){return this.eq(0);},last:function(){return this.eq(-1);},eq:function(i){var len=this.length,j=+i+(i<0?len:0);return this.pushStack(j>=0&&j<len?[this[j]]:[]);},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},end:function(){return this.prevObject||this.constructor(null);},push:core_push,sort:[].sort,splice:[].splice};jQuery.fn.init.prototype=jQuery.fn;jQuery.extend=jQuery.fn.extend=function(){var options,name,src,copy,copyIsArray,clone,target=arguments[0]||{},i=1,length=arguments.length,deep=false;if(typeof target==="boolean"){deep=target;target=arguments[1]||{};i=2;}
+if(typeof target!=="object"&&!jQuery.isFunction(target)){target={};}
+if(length===i){target=this;--i;}
+for(;i<length;i++){if((options=arguments[i])!=null){for(name in options){src=target[name];copy=options[name];if(target===copy){continue;}
+if(deep&&copy&&(jQuery.isPlainObject(copy)||(copyIsArray=jQuery.isArray(copy)))){if(copyIsArray){copyIsArray=false;clone=src&&jQuery.isArray(src)?src:[];}else{clone=src&&jQuery.isPlainObject(src)?src:{};}
+target[name]=jQuery.extend(deep,clone,copy);}else{if(copy!==undefined){target[name]=copy;}}}}}
+return target;};jQuery.extend({expando:"jQuery"+(core_version+Math.random()).replace(/\D/g,""),noConflict:function(deep){if(window.$===jQuery){window.$=_$;}
+if(deep&&window.jQuery===jQuery){window.jQuery=_jQuery;}
+return jQuery;},isReady:false,readyWait:1,holdReady:function(hold){if(hold){jQuery.readyWait++;}else{jQuery.ready(true);}},ready:function(wait){if(wait===true?--jQuery.readyWait:jQuery.isReady){return;}
+jQuery.isReady=true;if(wait!==true&&--jQuery.readyWait>0){return;}
+readyList.resolveWith(document,[jQuery]);if(jQuery.fn.trigger){jQuery(document).trigger("ready").off("ready");}},isFunction:function(obj){return jQuery.type(obj)==="function";},isArray:Array.isArray,isWindow:function(obj){return obj!=null&&obj===obj.window;},isNumeric:function(obj){return!isNaN(parseFloat(obj))&&isFinite(obj);},type:function(obj){if(obj==null){return String(obj);}
+return typeof obj==="object"||typeof obj==="function"?class2type[core_toString.call(obj)]||"object":typeof obj;},isPlainObject:function(obj){if(jQuery.type(obj)!=="object"||obj.nodeType||jQuery.isWindow(obj)){return false;}
+try{if(obj.constructor&&!core_hasOwn.call(obj.constructor.prototype,"isPrototypeOf")){return false;}}catch(e){return false;}
+return true;},isEmptyObject:function(obj){var name;for(name in obj){return false;}
+return true;},error:function(msg){throw new Error(msg);},parseHTML:function(data,context,keepScripts){if(!data||typeof data!=="string"){return null;}
+if(typeof context==="boolean"){keepScripts=context;context=false;}
+context=context||document;var parsed=rsingleTag.exec(data),scripts=!keepScripts&&[];if(parsed){return[context.createElement(parsed[1])];}
+parsed=jQuery.buildFragment([data],context,scripts);if(scripts){jQuery(scripts).remove();}
+return jQuery.merge([],parsed.childNodes);},parseJSON:JSON.parse,parseXML:function(data){var xml,tmp;if(!data||typeof data!=="string"){return null;}
+try{tmp=new DOMParser;xml=tmp.parseFromString(data,"text/xml");}catch(e){xml=undefined;}
+if(!xml||xml.getElementsByTagName("parsererror").length){jQuery.error("Invalid XML: "+data);}
+return xml;},noop:function(){},globalEval:function(code){var script,indirect=eval;code=jQuery.trim(code);if(code){if(code.indexOf("use strict")===1){script=document.createElement("script");script.text=code;document.head.appendChild(script).parentNode.removeChild(script);}else{indirect(code);}}},camelCase:function(string){return string.replace(rmsPrefix,"ms-").replace(rdashAlpha,fcamelCase);},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toLowerCase()===name.toLowerCase();},each:function(obj,callback,args){var value,i=0,length=obj.length,isArray=isArraylike(obj);if(args){if(isArray){for(;i<length;i++){value=callback.apply(obj[i],args);if(value===false){break;}}}else{for(i in obj){value=callback.apply(obj[i],args);if(value===false){break;}}}}else{if(isArray){for(;i<length;i++){value=callback.call(obj[i],i,obj[i]);if(value===false){break;}}}else{for(i in obj){value=callback.call(obj[i],i,obj[i]);if(value===false){break;}}}}
+return obj;},trim:function(text){return text==null?"":core_trim.call(text);},makeArray:function(arr,results){var ret=results||[];if(arr!=null){if(isArraylike(Object(arr))){jQuery.merge(ret,typeof arr==="string"?[arr]:arr);}else{core_push.call(ret,arr);}}
+return ret;},inArray:function(elem,arr,i){return arr==null?-1:core_indexOf.call(arr,elem,i);},merge:function(first,second){var l=second.length,i=first.length,j=0;if(typeof l==="number"){for(;j<l;j++){first[i++]=second[j];}}else{while(second[j]!==undefined){first[i++]=second[j++];}}
+first.length=i;return first;},grep:function(elems,callback,inv){var retVal,ret=[],i=0,length=elems.length;inv=!!inv;for(;i<length;i++){retVal=!!callback(elems[i],i);if(inv!==retVal){ret.push(elems[i]);}}
+return ret;},map:function(elems,callback,arg){var value,i=0,length=elems.length,isArray=isArraylike(elems),ret=[];if(isArray){for(;i<length;i++){value=callback(elems[i],i,arg);if(value!=null){ret[ret.length]=value;}}}else{for(i in elems){value=callback(elems[i],i,arg);if(value!=null){ret[ret.length]=value;}}}
+return core_concat.apply([],ret);},guid:1,proxy:function(fn,context){var tmp,args,proxy;if(typeof context==="string"){tmp=fn[context];context=fn;fn=tmp;}
+if(!jQuery.isFunction(fn)){return undefined;}
+args=core_slice.call(arguments,2);proxy=function(){return fn.apply(context||this,args.concat(core_slice.call(arguments)));};proxy.guid=fn.guid=fn.guid||jQuery.guid++;return proxy;},access:function(elems,fn,key,value,chainable,emptyGet,raw){var i=0,length=elems.length,bulk=key==null;if(jQuery.type(key)==="object"){chainable=true;for(i in key){jQuery.access(elems,fn,i,key[i],true,emptyGet,raw);}}else{if(value!==undefined){chainable=true;if(!jQuery.isFunction(value)){raw=true;}
+if(bulk){if(raw){fn.call(elems,value);fn=null;}else{bulk=fn;fn=function(elem,key,value){return bulk.call(jQuery(elem),value);};}}
+if(fn){for(;i<length;i++){fn(elems[i],key,raw?value:value.call(elems[i],i,fn(elems[i],key)));}}}}
+return chainable?elems:bulk?fn.call(elems):length?fn(elems[0],key):emptyGet;},now:Date.now,swap:function(elem,options,callback,args){var ret,name,old={};for(name in options){old[name]=elem.style[name];elem.style[name]=options[name];}
+ret=callback.apply(elem,args||[]);for(name in options){elem.style[name]=old[name];}
+return ret;}});jQuery.ready.promise=function(obj){if(!readyList){readyList=jQuery.Deferred();if(document.readyState==="complete"){setTimeout(jQuery.ready);}else{document.addEventListener("DOMContentLoaded",completed,false);window.addEventListener("load",completed,false);}}
+return readyList.promise(obj);};jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(i,name){class2type["[object "+name+"]"]=name.toLowerCase();});function isArraylike(obj){var length=obj.length,type=jQuery.type(obj);if(jQuery.isWindow(obj)){return false;}
+if(obj.nodeType===1&&length){return true;}
+return type==="array"||type!=="function"&&(length===0||typeof length==="number"&&length>0&&length-1 in obj);}
+rootjQuery=jQuery(document);(function(window,undefined){var i,support,cachedruns,Expr,getText,isXML,compile,outermostContext,sortInput,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+ -new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),hasDuplicate=false,sortOrder=function(){return 0;},strundefined=typeof undefined,MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=arr.indexOf||function(elem){var i=0,len=this.length;for(;i<len;i++){if(this[i]===elem){return i;}}
+return-1;},booleans="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",whitespace="[\\x20\\t\\r\\n\\f]",characterEncoding="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",identifier=characterEncoding.replace("w","w#"),attributes="\\["+whitespace+"*("+characterEncoding+")"+whitespace+"*(?:([*^$|!~]?=)"+whitespace+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+identifier+")|)|)"+whitespace+"*\\]",pseudos=":("+
+characterEncoding+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+attributes.replace(3,8)+")*)|.*)\\)|)",rtrim=new RegExp("^"+whitespace+"+|((?:^|[^\\\\])(?:\\\\.)*)"+whitespace+"+$","g"),rcomma=new RegExp("^"+whitespace+"*,"+whitespace+"*"),rcombinators=new RegExp("^"+whitespace+"*([>+~]|"+whitespace+")"+whitespace+"*"),rsibling=new RegExp(whitespace+"*[+~]"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*)"+
+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={"ID":new RegExp("^#("+characterEncoding+")"),"CLASS":new RegExp("^\\.("+characterEncoding+")"),"TAG":new RegExp("^("+characterEncoding.replace("w","w*")+")"),"ATTR":new RegExp("^"+attributes),"PSEUDO":new RegExp("^"+pseudos),"CHILD":new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),"bool":new RegExp("^(?:"+booleans+")$","i"),"needsContext":new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+
+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320);};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType;}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els));}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}
+target.length=j-1;}};}
+function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context);}
+context=context||document;results=results||[];if(!selector||typeof selector!=="string"){return results;}
+if((nodeType=context.nodeType)!==1&&nodeType!==9){return[];}
+if(documentIsHTML&&!seed){if(match=rquickExpr.exec(selector)){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results;}}else{return results;}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results;}}}else{if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results;}else{if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results;}}}}
+if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType===9&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&");}else{context.setAttribute("id",nid);}
+nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i]);}
+newContext=rsibling.test(selector)&&context.parentNode||context;newSelector=groups.join(",");}
+if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results;}catch(qsaError){}finally{if(!old){context.removeAttribute("id");}}}}}
+return select(selector.replace(rtrim,"$1"),context,results,seed);}
+function isNative(fn){return rnative.test(fn+"");}
+function createCache(){var keys=[];function cache(key,value){if(keys.push(key+=" ")>Expr.cacheLength){delete cache[keys.shift()];}
+return cache[key]=value;}
+return cache;}
+function markFunction(fn){fn[expando]=true;return fn;}
+function assert(fn){var div=document.createElement("div");try{return!!fn(div);}catch(e$0){return false;}finally{if(div.parentNode){div.parentNode.removeChild(div);}
+div=null;}}
+function addHandle(attrs,handler,test){attrs=attrs.split("|");var current,i=attrs.length,setHandle=test?null:handler;while(i--){if(!(current=Expr.attrHandle[attrs[i]])||current===handler){Expr.attrHandle[attrs[i]]=setHandle;}}}
+function boolHandler(elem,name){var val=elem.getAttributeNode(name);return val&&val.specified?val.value:elem[name]===true?name.toLowerCase():null;}
+function interpolationHandler(elem,name){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2);}
+function valueHandler(elem){if(elem.nodeName.toLowerCase()==="input"){return elem.defaultValue;}}
+function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff;}
+if(cur){while(cur=cur.nextSibling){if(cur===b){return-1;}}}
+return a?1:-1;}
+function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type;};}
+function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type;};}
+function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j]);}}});});}
+isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false;};support=Sizzle.support={};setDocument=Sizzle.setDocument=function(node){var doc=node?node.ownerDocument||node:preferredDoc,parent=doc.parentWindow;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document;}
+document=doc;docElem=doc.documentElement;documentIsHTML=!isXML(doc);if(parent&&parent.frameElement){parent.attachEvent("onbeforeunload",function(){setDocument();});}
+support.attributes=assert(function(div){div.innerHTML="<a href='#'></a>";addHandle("type|href|height|width",interpolationHandler,div.firstChild.getAttribute("href")==="#");addHandle(booleans,boolHandler,div.getAttribute("disabled")==null);div.className="i";return!div.getAttribute("className");});support.input=assert(function(div){div.innerHTML="<input>";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")==="";});addHandle("value",valueHandler,support.attributes&&support.input);support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length;});support.getElementsByClassName=assert(function(div){div.innerHTML="<div class='a'></div><div class='a i'></div>";div.firstChild.className="i";return div.getElementsByClassName("i").length===2;});support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length;});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!==strundefined&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[];}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId;};};}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!==strundefined&&elem.getAttributeNode("id");return node&&node.value===attrId;};};}
+Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!==strundefined){return context.getElementsByTagName(tag);}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem);}}
+return tmp;}
+return results;};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!==strundefined&&documentIsHTML){return context.getElementsByClassName(className);}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=isNative(doc.querySelectorAll)){assert(function(div){div.innerHTML="<select><option selected=''></option></select>";if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")");}
+if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked");}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("t","");if(div.querySelectorAll("[t^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")");}
+if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled");}
+div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:");});}
+if(support.matchesSelector=isNative(matches=docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos);});}
+rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));contains=isNative(docElem.contains)||docElem.compareDocumentPosition?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16));}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true;}}}
+return false;};support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(doc.createElement("div"))&1;});sortOrder=docElem.compareDocumentPosition?function(a,b){if(a===b){hasDuplicate=true;return 0;}
+var compare=b.compareDocumentPosition&&a.compareDocumentPosition&&a.compareDocumentPosition(b);if(compare){if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||contains(preferredDoc,a)){return-1;}
+if(b===doc||contains(preferredDoc,b)){return 1;}
+return sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0;}
+return compare&4?-1:1;}
+return a.compareDocumentPosition?-1:1;}:function(a,b){var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(a===b){hasDuplicate=true;return 0;}else{if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0;}else{if(aup===bup){return siblingCheck(a,b);}}}
+cur=a;while(cur=cur.parentNode){ap.unshift(cur);}
+cur=b;while(cur=cur.parentNode){bp.unshift(cur);}
+while(ap[i]===bp[i]){i++;}
+return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0;};return doc;};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements);};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem);}
+expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret;}}catch(e$1){}}
+return Sizzle(expr,document,null,[elem]).length>0;};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context);}
+return contains(context,elem);};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem);}
+var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val===undefined?support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null:val;};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg);};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i);}}
+while(j--){results.splice(duplicates[j],1);}}
+return results;};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){for(;node=elem[i];i++){ret+=getText(node);}}else{if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent;}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem);}}}else{if(nodeType===3||nodeType===4){return elem.nodeValue;}}}
+return ret;};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{"ATTR":function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" ";}
+return match.slice(0,4);},"CHILD":function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0]);}
+match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd");}else{if(match[3]){Sizzle.error(match[0]);}}
+return match;},"PSEUDO":function(match){var excess,unquoted=!match[5]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null;}
+if(match[3]&&match[4]!==undefined){match[2]=match[4];}else{if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess);}}
+return match.slice(0,3);}},filter:{"TAG":function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true;}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName;};},"CLASS":function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!==strundefined&&elem.getAttribute("class")||"");});},"ATTR":function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!=";}
+if(!operator){return true;}
+result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false;};},"CHILD":function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode;}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false;}}
+start=dir=type==="only"&&!start&&"nextSibling";}
+return true;}
+start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break;}}}else{if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1];}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff];}
+if(node===elem){break;}}}}}
+diff-=last;return diff===first||diff%first===0&&diff / first>=0;}};},"PSEUDO":function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument);}
+if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf.call(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i]);}}):function(elem){return fn(elem,0,args);};}
+return fn;}},pseudos:{"not":markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem);}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);return!results.pop();};}),"has":markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0;};}),"contains":markFunction(function(text){return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1;};}),"lang":markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang);}
+lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0;}}while((elem=elem.parentNode)&&elem.nodeType===1);return false;};}),"target":function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id;},"root":function(elem){return elem===docElem;},"focus":function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex);},"enabled":function(elem){return elem.disabled===false;},"disabled":function(elem){return elem.disabled===true;},"checked":function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected;},"selected":function(elem){if(elem.parentNode){elem.parentNode.selectedIndex;}
+return elem.selected===true;},"empty":function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeName>"@"||elem.nodeType===3||elem.nodeType===4){return false;}}
+return true;},"parent":function(elem){return!Expr.pseudos["empty"](elem);},"header":function(elem){return rheader.test(elem.nodeName);},"input":function(elem){return rinputs.test(elem.nodeName);},"button":function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button";},"text":function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()===elem.type);},"first":createPositionalPseudo(function(){return[0];}),"last":createPositionalPseudo(function(matchIndexes,length){return[length-1];}),"eq":createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument];}),"even":createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i<length;i+=2){matchIndexes.push(i);}
+return matchIndexes;}),"odd":createPositionalPseudo(function(matchIndexes,length){var i=1;for(;i<length;i+=2){matchIndexes.push(i);}
+return matchIndexes;}),"lt":createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;--i>=0;){matchIndexes.push(i);}
+return matchIndexes;}),"gt":createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i<length;){matchIndexes.push(i);}
+return matchIndexes;})}};for(i in{radio:true,checkbox:true,file:true,password:true,image:true}){Expr.pseudos[i]=createInputPseudo(i);}
+for(i in{submit:true,reset:true}){Expr.pseudos[i]=createButtonPseudo(i);}
+function tokenize(selector,parseOnly){var matched,match,tokens,type,soFar,groups,preFilters,cached=tokenCache[selector+" "];if(cached){return parseOnly?0:cached.slice(0);}
+soFar=selector;groups=[];preFilters=Expr.preFilter;while(soFar){if(!matched||(match=rcomma.exec(soFar))){if(match){soFar=soFar.slice(match[0].length)||soFar;}
+groups.push(tokens=[]);}
+matched=false;if(match=rcombinators.exec(soFar)){matched=match.shift();tokens.push({value:matched,type:match[0].replace(rtrim," ")});soFar=soFar.slice(matched.length);}
+for(type in Expr.filter){if((match=matchExpr[type].exec(soFar))&&(!preFilters[type]||(match=preFilters[type](match)))){matched=match.shift();tokens.push({value:matched,type:type,matches:match});soFar=soFar.slice(matched.length);}}
+if(!matched){break;}}
+return parseOnly?soFar.length:soFar?Sizzle.error(selector):tokenCache(selector,groups).slice(0);}
+function toSelector(tokens){var i=0,len=tokens.length,selector="";for(;i<len;i++){selector+=tokens[i].value;}
+return selector;}
+function addCombinator(matcher,combinator,base){var dir=combinator.dir,checkNonElements=base&&dir==="parentNode",doneName=done++;return combinator.first?function(elem,context,xml){while(elem=elem[dir]){if(elem.nodeType===1||checkNonElements){return matcher(elem,context,xml);}}}:function(elem,context,xml){var data,cache,outerCache,dirkey=dirruns+" "+doneName;if(xml){while(elem=elem[dir]){if(elem.nodeType===1||checkNonElements){if(matcher(elem,context,xml)){return true;}}}}else{while(elem=elem[dir]){if(elem.nodeType===1||checkNonElements){outerCache=elem[expando]||(elem[expando]={});if((cache=outerCache[dir])&&cache[0]===dirkey){if((data=cache[1])===true||data===cachedruns){return data===true;}}else{cache=outerCache[dir]=[dirkey];cache[1]=matcher(elem,context,xml)||cachedruns;if(cache[1]===true){return true;}}}}}};}
+function elementMatcher(matchers){return matchers.length>1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false;}}
+return true;}:matchers[0];}
+function condense(unmatched,map,filter,context,xml){var elem,newUnmatched=[],i=0,len=unmatched.length,mapped=map!=null;for(;i<len;i++){if(elem=unmatched[i]){if(!filter||filter(elem,context,xml)){newUnmatched.push(elem);if(mapped){map.push(i);}}}}
+return newUnmatched;}
+function setMatcher(preFilter,selector,matcher,postFilter,postFinder,postSelector){if(postFilter&&!postFilter[expando]){postFilter=setMatcher(postFilter);}
+if(postFinder&&!postFinder[expando]){postFinder=setMatcher(postFinder,postSelector);}
+return markFunction(function(seed,results,context,xml){var temp,i,elem,preMap=[],postMap=[],preexisting=results.length,elems=seed||multipleContexts(selector||"*",context.nodeType?[context]:context,[]),matcherIn=preFilter&&(seed||!selector)?condense(elems,preMap,preFilter,context,xml):elems,matcherOut=matcher?postFinder||(seed?preFilter:preexisting||postFilter)?[]:results:matcherIn;if(matcher){matcher(matcherIn,matcherOut,context,xml);}
+if(postFilter){temp=condense(matcherOut,postMap);postFilter(temp,[],context,xml);i=temp.length;while(i--){if(elem=temp[i]){matcherOut[postMap[i]]=!(matcherIn[postMap[i]]=elem);}}}
+if(seed){if(postFinder||preFilter){if(postFinder){temp=[];i=matcherOut.length;while(i--){if(elem=matcherOut[i]){temp.push(matcherIn[i]=elem);}}
+postFinder(null,matcherOut=[],temp,xml);}
+i=matcherOut.length;while(i--){if((elem=matcherOut[i])&&(temp=postFinder?indexOf.call(seed,elem):preMap[i])>-1){seed[temp]=!(results[temp]=elem);}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml);}else{push.apply(results,matcherOut);}}});}
+function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext;},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf.call(checkContext,elem)>-1;},implicitRelative,true),matchers=[function(elem,context,xml){return!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml));}];for(;i<len;i++){if(matcher=Expr.relative[tokens[i].type]){matchers=[addCombinator(elementMatcher(matchers),matcher)];}else{matcher=Expr.filter[tokens[i].type].apply(null,tokens[i].matches);if(matcher[expando]){j=++i;for(;j<len;j++){if(Expr.relative[tokens[j].type]){break;}}
+return setMatcher(i>1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i<j&&matcherFromTokens(tokens.slice(i,j)),j<len&&matcherFromTokens(tokens=tokens.slice(j)),j<len&&toSelector(tokens));}
+matchers.push(matcher);}}
+return elementMatcher(matchers);}
+function matcherFromGroupMatchers(elementMatchers,setMatchers){var matcherCachedRuns=0,bySet=setMatchers.length>0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,expandContext){var elem,j,matcher,setMatched=[],matchedCount=0,i="0",unmatched=seed&&[],outermost=expandContext!=null,contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",expandContext&&context.parentNode||context),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1;if(outermost){outermostContext=context!==document&&context;cachedruns=matcherCachedRuns;}
+for(;(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break;}}
+if(outermost){dirruns=dirrunsUnique;cachedruns=++matcherCachedRuns;}}
+if(bySet){if(elem=!matcher&&elem){matchedCount--;}
+if(seed){unmatched.push(elem);}}}
+matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml);}
+if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results);}}}
+setMatched=condense(setMatched);}
+push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results);}}
+if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup;}
+return unmatched;};return bySet?markFunction(superMatcher):superMatcher;}
+compile=Sizzle.compile=function(selector,group){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!group){group=tokenize(selector);}
+i=group.length;while(i--){cached=matcherFromTokens(group[i]);if(cached[expando]){setMatchers.push(cached);}else{elementMatchers.push(cached);}}
+cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers));}
+return cached;};function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i<len;i++){Sizzle(selector,contexts[i],results);}
+return results;}
+function select(selector,context,results,seed){var i,tokens,token,type,find,match=tokenize(selector);if(!seed){if(match.length===1){tokens=match[0]=match[0].slice(0);if(tokens.length>2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results;}
+selector=selector.slice(tokens.shift().value.length);}
+i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break;}
+if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&context.parentNode||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results;}
+break;}}}}}
+compile(selector,match)(seed,context,!documentIsHTML,results,rsibling.test(selector));return results;}
+Expr.pseudos["nth"]=Expr.pseudos["eq"];function setFilters(){}
+setFilters.prototype=Expr.filters=Expr.pseudos;Expr.setFilters=new setFilters;support.sortStable=expando.split("").sort(sortOrder).join("")===expando;setDocument();[0,0].sort(sortOrder);support.detectDuplicates=hasDuplicate;jQuery.find=Sizzle;jQuery.expr=Sizzle.selectors;jQuery.expr[":"]=jQuery.expr.pseudos;jQuery.unique=Sizzle.uniqueSort;jQuery.text=Sizzle.getText;jQuery.isXMLDoc=Sizzle.isXML;jQuery.contains=Sizzle.contains;})(window);var optionsCache={};function createOptions(options){var object=optionsCache[options]={};jQuery.each(options.match(core_rnotwhite)||[],function(_,flag){object[flag]=true;});return object;}
+jQuery.Callbacks=function(options){options=typeof options==="string"?optionsCache[options]||createOptions(options):jQuery.extend({},options);var memory,fired,firing,firingStart,firingLength,firingIndex,list=[],stack=!options.once&&[],fire=function(data){memory=options.memory&&data;fired=true;firingIndex=firingStart||0;firingStart=0;firingLength=list.length;firing=true;for(;list&&firingIndex<firingLength;firingIndex++){if(list[firingIndex].apply(data[0],data[1])===false&&options.stopOnFalse){memory=false;break;}}
+firing=false;if(list){if(stack){if(stack.length){fire(stack.shift());}}else{if(memory){list=[];}else{self.disable();}}}},self={add:function(){if(list){var start=list.length;(function add(args){jQuery.each(args,function(_,arg){var type=jQuery.type(arg);if(type==="function"){if(!options.unique||!self.has(arg)){list.push(arg);}}else{if(arg&&arg.length&&type!=="string"){add(arg);}}});})(arguments);if(firing){firingLength=list.length;}else{if(memory){firingStart=start;fire(memory);}}}
+return this;},remove:function(){if(list){jQuery.each(arguments,function(_,arg){var index;while((index=jQuery.inArray(arg,list,index))>-1){list.splice(index,1);if(firing){if(index<=firingLength){firingLength--;}
+if(index<=firingIndex){firingIndex--;}}}});}
+return this;},has:function(fn){return fn?jQuery.inArray(fn,list)>-1:!!(list&&list.length);},empty:function(){list=[];firingLength=0;return this;},disable:function(){list=stack=memory=undefined;return this;},disabled:function(){return!list;},lock:function(){stack=undefined;if(!memory){self.disable();}
+return this;},locked:function(){return!stack;},fireWith:function(context,args){args=args||[];args=[context,args.slice?args.slice():args];if(list&&(!fired||stack)){if(firing){stack.push(args);}else{fire(args);}}
+return this;},fire:function(){self.fireWith(this,arguments);return this;},fired:function(){return!!fired;}};return self;};jQuery.extend({Deferred:function(func){var tuples=[["resolve","done",jQuery.Callbacks("once memory"),"resolved"],["reject","fail",jQuery.Callbacks("once memory"),"rejected"],["notify","progress",jQuery.Callbacks("memory")]],state="pending",promise={state:function(){return state;},always:function(){deferred.done(arguments).fail(arguments);return this;},then:function(){var fns=arguments;return jQuery.Deferred(function(newDefer){jQuery.each(tuples,function(i,tuple){var action=tuple[0],fn=jQuery.isFunction(fns[i])&&fns[i];deferred[tuple[1]](function(){var returned=fn&&fn.apply(this,arguments);if(returned&&jQuery.isFunction(returned.promise)){returned.promise().done(newDefer.resolve).fail(newDefer.reject).progress(newDefer.notify);}else{newDefer[action+"With"](this===promise?newDefer.promise():this,fn?[returned]:arguments);}});});fns=null;}).promise();},promise:function(obj){return obj!=null?jQuery.extend(obj,promise):promise;}},deferred={};promise.pipe=promise.then;jQuery.each(tuples,function(i,tuple){var list=tuple[2],stateString=tuple[3];promise[tuple[1]]=list.add;if(stateString){list.add(function(){state=stateString;},tuples[i^1][2].disable,tuples[2][2].lock);}
+deferred[tuple[0]]=function(){deferred[tuple[0]+"With"](this===deferred?promise:this,arguments);return this;};deferred[tuple[0]+"With"]=list.fireWith;});promise.promise(deferred);if(func){func.call(deferred,deferred);}
+return deferred;},when:function(subordinate){var i=0,resolveValues=core_slice.call(arguments),length=resolveValues.length,remaining=length!==1||subordinate&&jQuery.isFunction(subordinate.promise)?length:0,deferred=remaining===1?subordinate:jQuery.Deferred(),updateFunc=function(i,contexts,values){return function(value){contexts[i]=this;values[i]=arguments.length>1?core_slice.call(arguments):value;if(values===progressValues){deferred.notifyWith(contexts,values);}else{if(!--remaining){deferred.resolveWith(contexts,values);}}};},progressValues,progressContexts,resolveContexts;if(length>1){progressValues=new Array(length);progressContexts=new Array(length);resolveContexts=new Array(length);for(;i<length;i++){if(resolveValues[i]&&jQuery.isFunction(resolveValues[i].promise)){resolveValues[i].promise().done(updateFunc(i,resolveContexts,resolveValues)).fail(deferred.reject).progress(updateFunc(i,progressContexts,progressValues));}else{--remaining;}}}
+if(!remaining){deferred.resolveWith(resolveContexts,resolveValues);}
+return deferred.promise();}});jQuery.support=function(support){var input=document.createElement("input"),fragment=document.createDocumentFragment(),div=document.createElement("div"),select=document.createElement("select"),opt=select.appendChild(document.createElement("option"));if(!input.type){return support;}
+input.type="checkbox";support.checkOn=input.value!=="";support.optSelected=opt.selected;support.reliableMarginRight=true;support.boxSizingReliable=true;support.pixelPosition=false;input.checked=true;support.noCloneChecked=input.cloneNode(true).checked;select.disabled=true;support.optDisabled=!opt.disabled;input=document.createElement("input");input.value="t";input.type="radio";support.radioValue=input.value==="t";input.setAttribute("checked","t");input.setAttribute("name","t");fragment.appendChild(input);support.checkClone=fragment.cloneNode(true).cloneNode(true).lastChild.checked;support.focusinBubbles="onfocusin"in window;div.style.backgroundClip="content-box";div.cloneNode(true).style.backgroundClip="";support.clearCloneStyle=div.style.backgroundClip==="content-box";jQuery(function(){var container,marginDiv,divReset="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",body=document.getElementsByTagName("body")[0];if(!body){return;}
+container=document.createElement("div");container.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";body.appendChild(container).appendChild(div);div.innerHTML="";div.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%";jQuery.swap(body,body.style.zoom!=null?{zoom:1}:{},function(){support.boxSizing=div.offsetWidth===4;});if(window.getComputedStyle){support.pixelPosition=(window.getComputedStyle(div,null)||{}).top!=="1%";support.boxSizingReliable=(window.getComputedStyle(div,null)||{width:"4px"}).width==="4px";marginDiv=div.appendChild(document.createElement("div"));marginDiv.style.cssText=div.style.cssText=divReset;marginDiv.style.marginRight=marginDiv.style.width="0";div.style.width="1px";support.reliableMarginRight=!parseFloat((window.getComputedStyle(marginDiv,null)||{}).marginRight);}
+body.removeChild(container);});return support;}({});var data_user,data_priv,rbrace=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,rmultiDash=/([A-Z])/g;function Data(){Object.defineProperty(this.cache={},0,{get:function(){return{};}});this.expando=jQuery.expando+Math.random();}
+Data.uid=1;Data.accepts=function(owner){return owner.nodeType?owner.nodeType===1||owner.nodeType===9:true;};Data.prototype={key:function(owner){if(!Data.accepts(owner)){return 0;}
+var descriptor={},unlock=owner[this.expando];if(!unlock){unlock=Data.uid++;try{descriptor[this.expando]={value:unlock};Object.defineProperties(owner,descriptor);}catch(e){descriptor[this.expando]=unlock;jQuery.extend(owner,descriptor);}}
+if(!this.cache[unlock]){this.cache[unlock]={};}
+return unlock;},set:function(owner,data,value){var prop,unlock=this.key(owner),cache=this.cache[unlock];if(typeof data==="string"){cache[data]=value;}else{if(jQuery.isEmptyObject(cache)){jQuery.extend(this.cache[unlock],data);}else{for(prop in data){cache[prop]=data[prop];}}}
+return cache;},get:function(owner,key){var cache=this.cache[this.key(owner)];return key===undefined?cache:cache[key];},access:function(owner,key,value){if(key===undefined||key&&typeof key==="string"&&value===undefined){return this.get(owner,key);}
+this.set(owner,key,value);return value!==undefined?value:key;},remove:function(owner,key){var i,name,camel,unlock=this.key(owner),cache=this.cache[unlock];if(key===undefined){this.cache[unlock]={};}else{if(jQuery.isArray(key)){name=key.concat(key.map(jQuery.camelCase));}else{camel=jQuery.camelCase(key);if(key in cache){name=[key,camel];}else{name=camel;name=name in cache?[name]:name.match(core_rnotwhite)||[];}}
+i=name.length;while(i--){delete cache[name[i]];}}},hasData:function(owner){return!jQuery.isEmptyObject(this.cache[owner[this.expando]]||{});},discard:function(owner){if(owner[this.expando]){delete this.cache[owner[this.expando]];}}};data_user=new Data;data_priv=new Data;jQuery.extend({acceptData:Data.accepts,hasData:function(elem){return data_user.hasData(elem)||data_priv.hasData(elem);},data:function(elem,name,data){return data_user.access(elem,name,data);},removeData:function(elem,name){data_user.remove(elem,name);},_data:function(elem,name,data){return data_priv.access(elem,name,data);},_removeData:function(elem,name){data_priv.remove(elem,name);}});jQuery.fn.extend({data:function(key,value){var attrs,name,elem=this[0],i=0,data=null;if(key===undefined){if(this.length){data=data_user.get(elem);if(elem.nodeType===1&&!data_priv.get(elem,"hasDataAttrs")){attrs=elem.attributes;for(;i<attrs.length;i++){name=attrs[i].name;if(name.indexOf("data-")===0){name=jQuery.camelCase(name.slice(5));dataAttr(elem,name,data[name]);}}
+data_priv.set(elem,"hasDataAttrs",true);}}
+return data;}
+if(typeof key==="object"){return this.each(function(){data_user.set(this,key);});}
+return jQuery.access(this,function(value){var data,camelKey=jQuery.camelCase(key);if(elem&&value===undefined){data=data_user.get(elem,key);if(data!==undefined){return data;}
+data=data_user.get(elem,camelKey);if(data!==undefined){return data;}
+data=dataAttr(elem,camelKey,undefined);if(data!==undefined){return data;}
+return;}
+this.each(function(){var data=data_user.get(this,camelKey);data_user.set(this,camelKey,value);if(key.indexOf("-")!==-1&&data!==undefined){data_user.set(this,key,value);}});},null,value,arguments.length>1,null,true);},removeData:function(key){return this.each(function(){data_user.remove(this,key);});}});function dataAttr(elem,key,data){var name;if(data===undefined&&elem.nodeType===1){name="data-"+key.replace(rmultiDash,"-$1").toLowerCase();data=elem.getAttribute(name);if(typeof data==="string"){try{data=data==="true"?true:data==="false"?false:data==="null"?null:+data+""===data?+data:rbrace.test(data)?JSON.parse(data):data;}catch(e){}
+data_user.set(elem,key,data);}else{data=undefined;}}
+return data;}
+jQuery.extend({queue:function(elem,type,data){var queue;if(elem){type=(type||"fx")+"queue";queue=data_priv.get(elem,type);if(data){if(!queue||jQuery.isArray(data)){queue=data_priv.access(elem,type,jQuery.makeArray(data));}else{queue.push(data);}}
+return queue||[];}},dequeue:function(elem,type){type=type||"fx";var queue=jQuery.queue(elem,type),startLength=queue.length,fn=queue.shift(),hooks=jQuery._queueHooks(elem,type),next=function(){jQuery.dequeue(elem,type);};if(fn==="inprogress"){fn=queue.shift();startLength--;}
+if(fn){if(type==="fx"){queue.unshift("inprogress");}
+delete hooks.stop;fn.call(elem,next,hooks);}
+if(!startLength&&hooks){hooks.empty.fire();}},_queueHooks:function(elem,type){var key=type+"queueHooks";return data_priv.get(elem,key)||data_priv.access(elem,key,{empty:jQuery.Callbacks("once memory").add(function(){data_priv.remove(elem,[type+"queue",key]);})});}});jQuery.fn.extend({queue:function(type,data){var setter=2;if(typeof type!=="string"){data=type;type="fx";setter--;}
+if(arguments.length<setter){return jQuery.queue(this[0],type);}
+return data===undefined?this:this.each(function(){var queue=jQuery.queue(this,type,data);jQuery._queueHooks(this,type);if(type==="fx"&&queue[0]!=="inprogress"){jQuery.dequeue(this,type);}});},dequeue:function(type){return this.each(function(){jQuery.dequeue(this,type);});},delay:function(time,type){time=jQuery.fx?jQuery.fx.speeds[time]||time:time;type=type||"fx";return this.queue(type,function(next,hooks){var timeout=setTimeout(next,time);hooks.stop=function(){clearTimeout(timeout);};});},clearQueue:function(type){return this.queue(type||"fx",[]);},promise:function(type,obj){var tmp,count=1,defer=jQuery.Deferred(),elements=this,i=this.length,resolve=function(){if(!--count){defer.resolveWith(elements,[elements]);}};if(typeof type!=="string"){obj=type;type=undefined;}
+type=type||"fx";while(i--){tmp=data_priv.get(elements[i],type+"queueHooks");if(tmp&&tmp.empty){count++;tmp.empty.add(resolve);}}
+resolve();return defer.promise(obj);}});var nodeHook,boolHook,rclass=/[\t\r\n\f]/g,rreturn=/\r/g,rfocusable=/^(?:input|select|textarea|button)$/i;jQuery.fn.extend({attr:function(name,value){return jQuery.access(this,jQuery.attr,name,value,arguments.length>1);},removeAttr:function(name){return this.each(function(){jQuery.removeAttr(this,name);});},prop:function(name,value){return jQuery.access(this,jQuery.prop,name,value,arguments.length>1);},removeProp:function(name){return this.each(function(){delete this[jQuery.propFix[name]||name];});},addClass:function(value){var classes,elem,cur,clazz,j,i=0,len=this.length,proceed=typeof value==="string"&&value;if(jQuery.isFunction(value)){return this.each(function(j){jQuery(this).addClass(value.call(this,j,this.className));});}
+if(proceed){classes=(value||"").match(core_rnotwhite)||[];for(;i<len;i++){elem=this[i];cur=elem.nodeType===1&&(elem.className?(" "+elem.className+" ").replace(rclass," "):" ");if(cur){j=0;while(clazz=classes[j++]){if(cur.indexOf(" "+clazz+" ")<0){cur+=clazz+" ";}}
+elem.className=jQuery.trim(cur);}}}
+return this;},removeClass:function(value){var classes,elem,cur,clazz,j,i=0,len=this.length,proceed=arguments.length===0||typeof value==="string"&&value;if(jQuery.isFunction(value)){return this.each(function(j){jQuery(this).removeClass(value.call(this,j,this.className));});}
+if(proceed){classes=(value||"").match(core_rnotwhite)||[];for(;i<len;i++){elem=this[i];cur=elem.nodeType===1&&(elem.className?(" "+elem.className+" ").replace(rclass," "):"");if(cur){j=0;while(clazz=classes[j++]){while(cur.indexOf(" "+clazz+" ")>=0){cur=cur.replace(" "+clazz+" "," ");}}
+elem.className=value?jQuery.trim(cur):"";}}}
+return this;},toggleClass:function(value,stateVal){var type=typeof value,isBool=typeof stateVal==="boolean";if(jQuery.isFunction(value)){return this.each(function(i){jQuery(this).toggleClass(value.call(this,i,this.className,stateVal),stateVal);});}
+return this.each(function(){if(type==="string"){var className,i=0,self=jQuery(this),state=stateVal,classNames=value.match(core_rnotwhite)||[];while(className=classNames[i++]){state=isBool?state:!self.hasClass(className);self[state?"addClass":"removeClass"](className);}}else{if(type===core_strundefined||type==="boolean"){if(this.className){data_priv.set(this,"__className__",this.className);}
+this.className=this.className||value===false?"":data_priv.get(this,"__className__")||"";}}});},hasClass:function(selector){var className=" "+selector+" ",i=0,l=this.length;for(;i<l;i++){if(this[i].nodeType===1&&(" "+this[i].className+" ").replace(rclass," ").indexOf(className)>=0){return true;}}
+return false;},val:function(value){var hooks,ret,isFunction,elem=this[0];if(!arguments.length){if(elem){hooks=jQuery.valHooks[elem.type]||jQuery.valHooks[elem.nodeName.toLowerCase()];if(hooks&&"get"in hooks&&(ret=hooks.get(elem,"value"))!==undefined){return ret;}
+ret=elem.value;return typeof ret==="string"?ret.replace(rreturn,""):ret==null?"":ret;}
+return;}
+isFunction=jQuery.isFunction(value);return this.each(function(i){var val;if(this.nodeType!==1){return;}
+if(isFunction){val=value.call(this,i,jQuery(this).val());}else{val=value;}
+if(val==null){val="";}else{if(typeof val==="number"){val+="";}else{if(jQuery.isArray(val)){val=jQuery.map(val,function(value){return value==null?"":value+"";});}}}
+hooks=jQuery.valHooks[this.type]||jQuery.valHooks[this.nodeName.toLowerCase()];if(!hooks||!("set"in hooks)||hooks.set(this,val,"value")===undefined){this.value=val;}});}});jQuery.extend({valHooks:{option:{get:function(elem){var val=elem.attributes.value;return!val||val.specified?elem.value:elem.text;}},select:{get:function(elem){var value,option,options=elem.options,index=elem.selectedIndex,one=elem.type==="select-one"||index<0,values=one?null:[],max=one?index+1:options.length,i=index<0?max:one?index:0;for(;i<max;i++){option=options[i];if((option.selected||i===index)&&(jQuery.support.optDisabled?!option.disabled:option.getAttribute("disabled")===null)&&(!option.parentNode.disabled||!jQuery.nodeName(option.parentNode,"optgroup"))){value=jQuery(option).val();if(one){return value;}
+values.push(value);}}
+return values;},set:function(elem,value){var optionSet,option,options=elem.options,values=jQuery.makeArray(value),i=options.length;while(i--){option=options[i];if(option.selected=jQuery.inArray(jQuery(option).val(),values)>=0){optionSet=true;}}
+if(!optionSet){elem.selectedIndex=-1;}
+return values;}}},attr:function(elem,name,value){var hooks,ret,nType=elem.nodeType;if(!elem||nType===3||nType===8||nType===2){return;}
+if(typeof elem.getAttribute===core_strundefined){return jQuery.prop(elem,name,value);}
+if(nType!==1||!jQuery.isXMLDoc(elem)){name=name.toLowerCase();hooks=jQuery.attrHooks[name]||(jQuery.expr.match.bool.test(name)?boolHook:nodeHook);}
+if(value!==undefined){if(value===null){jQuery.removeAttr(elem,name);}else{if(hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined){return ret;}else{elem.setAttribute(name,value+"");return value;}}}else{if(hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null){return ret;}else{ret=jQuery.find.attr(elem,name);return ret==null?undefined:ret;}}},removeAttr:function(elem,value){var name,propName,i=0,attrNames=value&&value.match(core_rnotwhite);if(attrNames&&elem.nodeType===1){while(name=attrNames[i++]){propName=jQuery.propFix[name]||name;if(jQuery.expr.match.bool.test(name)){elem[propName]=false;}
+elem.removeAttribute(name);}}},attrHooks:{type:{set:function(elem,value){if(!jQuery.support.radioValue&&value==="radio"&&jQuery.nodeName(elem,"input")){var val=elem.value;elem.setAttribute("type",value);if(val){elem.value=val;}
+return value;}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(elem,name,value){var ret,hooks,notxml,nType=elem.nodeType;if(!elem||nType===3||nType===8||nType===2){return;}
+notxml=nType!==1||!jQuery.isXMLDoc(elem);if(notxml){name=jQuery.propFix[name]||name;hooks=jQuery.propHooks[name];}
+if(value!==undefined){return hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined?ret:elem[name]=value;}else{return hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null?ret:elem[name];}},propHooks:{tabIndex:{get:function(elem){return elem.hasAttribute("tabindex")||rfocusable.test(elem.nodeName)||elem.href?elem.tabIndex:-1;}}}});boolHook={set:function(elem,value,name){if(value===false){jQuery.removeAttr(elem,name);}else{elem.setAttribute(name,name);}
+return name;}};jQuery.each(jQuery.expr.match.bool.source.match(/\w+/g),function(i,name){var getter=jQuery.expr.attrHandle[name]||jQuery.find.attr;jQuery.expr.attrHandle[name]=function(elem,name,isXML){var fn=jQuery.expr.attrHandle[name],ret=isXML?undefined:(jQuery.expr.attrHandle[name]=undefined)!=getter(elem,name,isXML)?name.toLowerCase():null;jQuery.expr.attrHandle[name]=fn;return ret;};});if(!jQuery.support.optSelected){jQuery.propHooks.selected={get:function(elem){var parent=elem.parentNode;if(parent&&parent.parentNode){parent.parentNode.selectedIndex;}
+return null;}};}
+jQuery.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){jQuery.propFix[this.toLowerCase()]=this;});jQuery.each(["radio","checkbox"],function(){jQuery.valHooks[this]={set:function(elem,value){if(jQuery.isArray(value)){return elem.checked=jQuery.inArray(jQuery(elem).val(),value)>=0;}}};if(!jQuery.support.checkOn){jQuery.valHooks[this].get=function(elem){return elem.getAttribute("value")===null?"on":elem.value;};}});var rkeyEvent=/^key/,rmouseEvent=/^(?:mouse|contextmenu)|click/,rfocusMorph=/^(?:focusinfocus|focusoutblur)$/,rtypenamespace=/^([^.]*)(?:\.(.+)|)$/;function returnTrue(){return true;}
+function returnFalse(){return false;}
+function safeActiveElement(){try{return document.activeElement;}catch(err){}}
+jQuery.event={global:{},add:function(elem,types,handler,data,selector){var handleObjIn,eventHandle,tmp,events,t,handleObj,special,handlers,type,namespaces,origType,elemData=data_priv.get(elem);if(!elemData){return;}
+if(handler.handler){handleObjIn=handler;handler=handleObjIn.handler;selector=handleObjIn.selector;}
+if(!handler.guid){handler.guid=jQuery.guid++;}
+if(!(events=elemData.events)){events=elemData.events={};}
+if(!(eventHandle=elemData.handle)){eventHandle=elemData.handle=function(e){return typeof jQuery!==core_strundefined&&(!e||jQuery.event.triggered!==e.type)?jQuery.event.dispatch.apply(eventHandle.elem,arguments):undefined;};eventHandle.elem=elem;}
+types=(types||"").match(core_rnotwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){continue;}
+special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;special=jQuery.event.special[type]||{};handleObj=jQuery.extend({type:type,origType:origType,data:data,handler:handler,guid:handler.guid,selector:selector,needsContext:selector&&jQuery.expr.match.needsContext.test(selector),namespace:namespaces.join(".")},handleObjIn);if(!(handlers=events[type])){handlers=events[type]=[];handlers.delegateCount=0;if(!special.setup||special.setup.call(elem,data,namespaces,eventHandle)===false){if(elem.addEventListener){elem.addEventListener(type,eventHandle,false);}}}
+if(special.add){special.add.call(elem,handleObj);if(!handleObj.handler.guid){handleObj.handler.guid=handler.guid;}}
+if(selector){handlers.splice(handlers.delegateCount++,0,handleObj);}else{handlers.push(handleObj);}
+jQuery.event.global[type]=true;}
+elem=null;},remove:function(elem,types,handler,selector,mappedTypes){var j,origCount,tmp,events,t,handleObj,special,handlers,type,namespaces,origType,elemData=data_priv.hasData(elem)&&data_priv.get(elem);if(!elemData||!(events=elemData.events)){return;}
+types=(types||"").match(core_rnotwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){for(type in events){jQuery.event.remove(elem,type+types[t],handler,selector,true);}
+continue;}
+special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;handlers=events[type]||[];tmp=tmp[2]&&new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)");origCount=j=handlers.length;while(j--){handleObj=handlers[j];if((mappedTypes||origType===handleObj.origType)&&(!handler||handler.guid===handleObj.guid)&&(!tmp||tmp.test(handleObj.namespace))&&(!selector||selector===handleObj.selector||selector==="**"&&handleObj.selector)){handlers.splice(j,1);if(handleObj.selector){handlers.delegateCount--;}
+if(special.remove){special.remove.call(elem,handleObj);}}}
+if(origCount&&!handlers.length){if(!special.teardown||special.teardown.call(elem,namespaces,elemData.handle)===false){jQuery.removeEvent(elem,type,elemData.handle);}
+delete events[type];}}
+if(jQuery.isEmptyObject(events)){delete elemData.handle;data_priv.remove(elem,"events");}},trigger:function(event,data,elem,onlyHandlers){var i,cur,tmp,bubbleType,ontype,handle,special,eventPath=[elem||document],type=core_hasOwn.call(event,"type")?event.type:event,namespaces=core_hasOwn.call(event,"namespace")?event.namespace.split("."):[];cur=tmp=elem=elem||document;if(elem.nodeType===3||elem.nodeType===8){return;}
+if(rfocusMorph.test(type+jQuery.event.triggered)){return;}
+if(type.indexOf(".")>=0){namespaces=type.split(".");type=namespaces.shift();namespaces.sort();}
+ontype=type.indexOf(":")<0&&"on"+type;event=event[jQuery.expando]?event:new jQuery.Event(type,typeof event==="object"&&event);event.isTrigger=onlyHandlers?2:3;event.namespace=namespaces.join(".");event.namespace_re=event.namespace?new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)"):null;event.result=undefined;if(!event.target){event.target=elem;}
+data=data==null?[event]:jQuery.makeArray(data,[event]);special=jQuery.event.special[type]||{};if(!onlyHandlers&&special.trigger&&special.trigger.apply(elem,data)===false){return;}
+if(!onlyHandlers&&!special.noBubble&&!jQuery.isWindow(elem)){bubbleType=special.delegateType||type;if(!rfocusMorph.test(bubbleType+type)){cur=cur.parentNode;}
+for(;cur;cur=cur.parentNode){eventPath.push(cur);tmp=cur;}
+if(tmp===(elem.ownerDocument||document)){eventPath.push(tmp.defaultView||tmp.parentWindow||window);}}
+i=0;while((cur=eventPath[i++])&&!event.isPropagationStopped()){event.type=i>1?bubbleType:special.bindType||type;handle=(data_priv.get(cur,"events")||{})[event.type]&&data_priv.get(cur,"handle");if(handle){handle.apply(cur,data);}
+handle=ontype&&cur[ontype];if(handle&&jQuery.acceptData(cur)&&handle.apply&&handle.apply(cur,data)===false){event.preventDefault();}}
+event.type=type;if(!onlyHandlers&&!event.isDefaultPrevented()){if((!special._default||special._default.apply(eventPath.pop(),data)===false)&&jQuery.acceptData(elem)){if(ontype&&jQuery.isFunction(elem[type])&&!jQuery.isWindow(elem)){tmp=elem[ontype];if(tmp){elem[ontype]=null;}
+jQuery.event.triggered=type;elem[type]();jQuery.event.triggered=undefined;if(tmp){elem[ontype]=tmp;}}}}
+return event.result;},dispatch:function(event){event=jQuery.event.fix(event);var i,j,ret,matched,handleObj,handlerQueue=[],args=core_slice.call(arguments),handlers=(data_priv.get(this,"events")||{})[event.type]||[],special=jQuery.event.special[event.type]||{};args[0]=event;event.delegateTarget=this;if(special.preDispatch&&special.preDispatch.call(this,event)===false){return;}
+handlerQueue=jQuery.event.handlers.call(this,event,handlers);i=0;while((matched=handlerQueue[i++])&&!event.isPropagationStopped()){event.currentTarget=matched.elem;j=0;while((handleObj=matched.handlers[j++])&&!event.isImmediatePropagationStopped()){if(!event.namespace_re||event.namespace_re.test(handleObj.namespace)){event.handleObj=handleObj;event.data=handleObj.data;ret=((jQuery.event.special[handleObj.origType]||{}).handle||handleObj.handler).apply(matched.elem,args);if(ret!==undefined){if((event.result=ret)===false){event.preventDefault();event.stopPropagation();}}}}}
+if(special.postDispatch){special.postDispatch.call(this,event);}
+return event.result;},handlers:function(event,handlers){var i,matches,sel,handleObj,handlerQueue=[],delegateCount=handlers.delegateCount,cur=event.target;if(delegateCount&&cur.nodeType&&(!event.button||event.type!=="click")){for(;cur!==this;cur=cur.parentNode||this){if(cur.disabled!==true||event.type!=="click"){matches=[];for(i=0;i<delegateCount;i++){handleObj=handlers[i];sel=handleObj.selector+" ";if(matches[sel]===undefined){matches[sel]=handleObj.needsContext?jQuery(sel,this).index(cur)>=0:jQuery.find(sel,this,null,[cur]).length;}
+if(matches[sel]){matches.push(handleObj);}}
+if(matches.length){handlerQueue.push({elem:cur,handlers:matches});}}}}
+if(delegateCount<handlers.length){handlerQueue.push({elem:this,handlers:handlers.slice(delegateCount)});}
+return handlerQueue;},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(event,original){if(event.which==null){event.which=original.charCode!=null?original.charCode:original.keyCode;}
+return event;}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(event,original){var eventDoc,doc,body,button=original.button;if(event.pageX==null&&original.clientX!=null){eventDoc=event.target.ownerDocument||document;doc=eventDoc.documentElement;body=eventDoc.body;event.pageX=original.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc&&doc.clientLeft||body&&body.clientLeft||0);event.pageY=original.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc&&doc.clientTop||body&&body.clientTop||0);}
+if(!event.which&&button!==undefined){event.which=button&1?1:button&2?3:button&4?2:0;}
+return event;}},fix:function(event){if(event[jQuery.expando]){return event;}
+var i,prop,copy,type=event.type,originalEvent=event,fixHook=this.fixHooks[type];if(!fixHook){this.fixHooks[type]=fixHook=rmouseEvent.test(type)?this.mouseHooks:rkeyEvent.test(type)?this.keyHooks:{};}
+copy=fixHook.props?this.props.concat(fixHook.props):this.props;event=new jQuery.Event(originalEvent);i=copy.length;while(i--){prop=copy[i];event[prop]=originalEvent[prop];}
+if(!event.target){event.target=document;}
+if(event.target.nodeType===3){event.target=event.target.parentNode;}
+return fixHook.filter?fixHook.filter(event,originalEvent):event;},special:{load:{noBubble:true},focus:{trigger:function(){if(this!==safeActiveElement()&&this.focus){this.focus();return false;}},delegateType:"focusin"},blur:{trigger:function(){if(this===safeActiveElement()&&this.blur){this.blur();return false;}},delegateType:"focusout"},click:{trigger:function(){if(this.type==="checkbox"&&this.click&&jQuery.nodeName(this,"input")){this.click();return false;}},_default:function(event){return jQuery.nodeName(event.target,"a");}},beforeunload:{postDispatch:function(event){if(event.result!==undefined){event.originalEvent.returnValue=event.result;}}}},simulate:function(type,elem,event,bubble){var e=jQuery.extend(new jQuery.Event,event,{type:type,isSimulated:true,originalEvent:{}});if(bubble){jQuery.event.trigger(e,null,elem);}else{jQuery.event.dispatch.call(elem,e);}
+if(e.isDefaultPrevented()){event.preventDefault();}}};jQuery.removeEvent=function(elem,type,handle){if(elem.removeEventListener){elem.removeEventListener(type,handle,false);}};jQuery.Event=function(src,props){if(!(this instanceof jQuery.Event)){return new jQuery.Event(src,props);}
+if(src&&src.type){this.originalEvent=src;this.type=src.type;this.isDefaultPrevented=src.defaultPrevented||src.getPreventDefault&&src.getPreventDefault()?returnTrue:returnFalse;}else{this.type=src;}
+if(props){jQuery.extend(this,props);}
+this.timeStamp=src&&src.timeStamp||jQuery.now();this[jQuery.expando]=true;};jQuery.Event.prototype={isDefaultPrevented:returnFalse,isPropagationStopped:returnFalse,isImmediatePropagationStopped:returnFalse,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=returnTrue;if(e&&e.preventDefault){e.preventDefault();}},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=returnTrue;if(e&&e.stopPropagation){e.stopPropagation();}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=returnTrue;this.stopPropagation();}};jQuery.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(orig,fix){jQuery.event.special[orig]={delegateType:fix,bindType:fix,handle:function(event){var ret,target=this,related=event.relatedTarget,handleObj=event.handleObj;if(!related||related!==target&&!jQuery.contains(target,related)){event.type=handleObj.origType;ret=handleObj.handler.apply(this,arguments);event.type=fix;}
+return ret;}};});if(!jQuery.support.focusinBubbles){jQuery.each({focus:"focusin",blur:"focusout"},function(orig,fix){var attaches=0,handler=function(event){jQuery.event.simulate(fix,event.target,jQuery.event.fix(event),true);};jQuery.event.special[fix]={setup:function(){if(attaches++===0){document.addEventListener(orig,handler,true);}},teardown:function(){if(--attaches===0){document.removeEventListener(orig,handler,true);}}};});}
+jQuery.fn.extend({on:function(types,selector,data,fn,one){var origFn,type;if(typeof types==="object"){if(typeof selector!=="string"){data=data||selector;selector=undefined;}
+for(type in types){this.on(type,selector,data,types[type],one);}
+return this;}
+if(data==null&&fn==null){fn=selector;data=selector=undefined;}else{if(fn==null){if(typeof selector==="string"){fn=data;data=undefined;}else{fn=data;data=selector;selector=undefined;}}}
+if(fn===false){fn=returnFalse;}else{if(!fn){return this;}}
+if(one===1){origFn=fn;fn=function(event){jQuery().off(event);return origFn.apply(this,arguments);};fn.guid=origFn.guid||(origFn.guid=jQuery.guid++);}
+return this.each(function(){jQuery.event.add(this,types,fn,data,selector);});},one:function(types,selector,data,fn){return this.on(types,selector,data,fn,1);},off:function(types,selector,fn){var handleObj,type;if(types&&types.preventDefault&&types.handleObj){handleObj=types.handleObj;jQuery(types.delegateTarget).off(handleObj.namespace?handleObj.origType+"."+handleObj.namespace:handleObj.origType,handleObj.selector,handleObj.handler);return this;}
+if(typeof types==="object"){for(type in types){this.off(type,selector,types[type]);}
+return this;}
+if(selector===false||typeof selector==="function"){fn=selector;selector=undefined;}
+if(fn===false){fn=returnFalse;}
+return this.each(function(){jQuery.event.remove(this,types,fn,selector);});},trigger:function(type,data){return this.each(function(){jQuery.event.trigger(type,data,this);});},triggerHandler:function(type,data){var elem=this[0];if(elem){return jQuery.event.trigger(type,data,elem,true);}}});var isSimple=/^.[^:#\[\.,]*$/,rparentsprev=/^(?:parents|prev(?:Until|All))/,rneedsContext=jQuery.expr.match.needsContext,guaranteedUnique={children:true,contents:true,next:true,prev:true};jQuery.fn.extend({find:function(selector){var i,ret=[],self=this,len=self.length;if(typeof selector!=="string"){return this.pushStack(jQuery(selector).filter(function(){for(i=0;i<len;i++){if(jQuery.contains(self[i],this)){return true;}}}));}
+for(i=0;i<len;i++){jQuery.find(selector,self[i],ret);}
+ret=this.pushStack(len>1?jQuery.unique(ret):ret);ret.selector=this.selector?this.selector+" "+selector:selector;return ret;},has:function(target){var targets=jQuery(target,this),l=targets.length;return this.filter(function(){var i=0;for(;i<l;i++){if(jQuery.contains(this,targets[i])){return true;}}});},not:function(selector){return this.pushStack(winnow(this,selector||[],true));},filter:function(selector){return this.pushStack(winnow(this,selector||[],false));},is:function(selector){return!!winnow(this,typeof selector==="string"&&rneedsContext.test(selector)?jQuery(selector):selector||[],false).length;},closest:function(selectors,context){var cur,i=0,l=this.length,matched=[],pos=rneedsContext.test(selectors)||typeof selectors!=="string"?jQuery(selectors,context||this.context):0;for(;i<l;i++){for(cur=this[i];cur&&cur!==context;cur=cur.parentNode){if(cur.nodeType<11&&(pos?pos.index(cur)>-1:cur.nodeType===1&&jQuery.find.matchesSelector(cur,selectors))){cur=matched.push(cur);break;}}}
+return this.pushStack(matched.length>1?jQuery.unique(matched):matched);},index:function(elem){if(!elem){return this[0]&&this[0].parentNode?this.first().prevAll().length:-1;}
+if(typeof elem==="string"){return core_indexOf.call(jQuery(elem),this[0]);}
+return core_indexOf.call(this,elem.jquery?elem[0]:elem);},add:function(selector,context){var set=typeof selector==="string"?jQuery(selector,context):jQuery.makeArray(selector&&selector.nodeType?[selector]:selector),all=jQuery.merge(this.get(),set);return this.pushStack(jQuery.unique(all));},addBack:function(selector){return this.add(selector==null?this.prevObject:this.prevObject.filter(selector));}});function sibling(cur,dir){while((cur=cur[dir])&&cur.nodeType!==1){}
+return cur;}
+jQuery.each({parent:function(elem){var parent=elem.parentNode;return parent&&parent.nodeType!==11?parent:null;},parents:function(elem){return jQuery.dir(elem,"parentNode");},parentsUntil:function(elem,i,until){return jQuery.dir(elem,"parentNode",until);},next:function(elem){return sibling(elem,"nextSibling");},prev:function(elem){return sibling(elem,"previousSibling");},nextAll:function(elem){return jQuery.dir(elem,"nextSibling");},prevAll:function(elem){return jQuery.dir(elem,"previousSibling");},nextUntil:function(elem,i,until){return jQuery.dir(elem,"nextSibling",until);},prevUntil:function(elem,i,until){return jQuery.dir(elem,"previousSibling",until);},siblings:function(elem){return jQuery.sibling((elem.parentNode||{}).firstChild,elem);},children:function(elem){return jQuery.sibling(elem.firstChild);},contents:function(elem){return elem.contentDocument||jQuery.merge([],elem.childNodes);}},function(name,fn){jQuery.fn[name]=function(until,selector){var matched=jQuery.map(this,fn,until);if(name.slice(-5)!=="Until"){selector=until;}
+if(selector&&typeof selector==="string"){matched=jQuery.filter(selector,matched);}
+if(this.length>1){if(!guaranteedUnique[name]){jQuery.unique(matched);}
+if(rparentsprev.test(name)){matched.reverse();}}
+return this.pushStack(matched);};});jQuery.extend({filter:function(expr,elems,not){var elem=elems[0];if(not){expr=":not("+expr+")";}
+return elems.length===1&&elem.nodeType===1?jQuery.find.matchesSelector(elem,expr)?[elem]:[]:jQuery.find.matches(expr,jQuery.grep(elems,function(elem){return elem.nodeType===1;}));},dir:function(elem,dir,until){var matched=[],truncate=until!==undefined;while((elem=elem[dir])&&elem.nodeType!==9){if(elem.nodeType===1){if(truncate&&jQuery(elem).is(until)){break;}
+matched.push(elem);}}
+return matched;},sibling:function(n,elem){var matched=[];for(;n;n=n.nextSibling){if(n.nodeType===1&&n!==elem){matched.push(n);}}
+return matched;}});function winnow(elements,qualifier,not){if(jQuery.isFunction(qualifier)){return jQuery.grep(elements,function(elem,i){return!!qualifier.call(elem,i,elem)!==not;});}
+if(qualifier.nodeType){return jQuery.grep(elements,function(elem){return elem===qualifier!==not;});}
+if(typeof qualifier==="string"){if(isSimple.test(qualifier)){return jQuery.filter(qualifier,elements,not);}
+qualifier=jQuery.filter(qualifier,elements);}
+return jQuery.grep(elements,function(elem){return core_indexOf.call(qualifier,elem)>=0!==not;});}
+var rxhtmlTag=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,rtagName=/<([\w:]+)/,rhtml=/<|&#?\w+;/,rnoInnerhtml=/<(?:script|style|link)/i,manipulation_rcheckableType=/^(?:checkbox|radio)$/i,rchecked=/checked\s*(?:[^=]|=\s*.checked.)/i,rscriptType=/^$|\/(?:java|ecma)script/i,rscriptTypeMasked=/^true\/(.*)/,rcleanScript=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,wrapMap={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};wrapMap.optgroup=wrapMap.option;wrapMap.tbody=wrapMap.tfoot=wrapMap.colgroup=wrapMap.caption=wrapMap.thead;wrapMap.th=wrapMap.td;jQuery.fn.extend({text:function(value){return jQuery.access(this,function(value){return value===undefined?jQuery.text(this):this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(value));},null,value,arguments.length);},append:function(){return this.domManip(arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.appendChild(elem);}});},prepend:function(){return this.domManip(arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.insertBefore(elem,target.firstChild);}});},before:function(){return this.domManip(arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this);}});},after:function(){return this.domManip(arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this.nextSibling);}});},remove:function(selector,keepData){var elem,elems=selector?jQuery.filter(selector,this):this,i=0;for(;(elem=elems[i])!=null;i++){if(!keepData&&elem.nodeType===1){jQuery.cleanData(getAll(elem));}
+if(elem.parentNode){if(keepData&&jQuery.contains(elem.ownerDocument,elem)){setGlobalEval(getAll(elem,"script"));}
+elem.parentNode.removeChild(elem);}}
+return this;},empty:function(){var elem,i=0;for(;(elem=this[i])!=null;i++){if(elem.nodeType===1){jQuery.cleanData(getAll(elem,false));elem.textContent="";}}
+return this;},clone:function(dataAndEvents,deepDataAndEvents){dataAndEvents=dataAndEvents==null?false:dataAndEvents;deepDataAndEvents=deepDataAndEvents==null?dataAndEvents:deepDataAndEvents;return this.map(function(){return jQuery.clone(this,dataAndEvents,deepDataAndEvents);});},html:function(value){return jQuery.access(this,function(value){var elem=this[0]||{},i=0,l=this.length;if(value===undefined&&elem.nodeType===1){return elem.innerHTML;}
+if(typeof value==="string"&&!rnoInnerhtml.test(value)&&!wrapMap[(rtagName.exec(value)||["",""])[1].toLowerCase()]){value=value.replace(rxhtmlTag,"<$1></$2>");try{for(;i<l;i++){elem=this[i]||{};if(elem.nodeType===1){jQuery.cleanData(getAll(elem,false));elem.innerHTML=value;}}
+elem=0;}catch(e){}}
+if(elem){this.empty().append(value);}},null,value,arguments.length);},replaceWith:function(){var args=jQuery.map(this,function(elem){return[elem.nextSibling,elem.parentNode];}),i=0;this.domManip(arguments,function(elem){var next=args[i++],parent=args[i++];if(parent){if(next&&next.parentNode!==parent){next=this.nextSibling;}
+jQuery(this).remove();parent.insertBefore(elem,next);}},true);return i?this:this.remove();},detach:function(selector){return this.remove(selector,true);},domManip:function(args,callback,allowIntersection){args=core_concat.apply([],args);var fragment,first,scripts,hasScripts,node,doc,i=0,l=this.length,set=this,iNoClone=l-1,value=args[0],isFunction=jQuery.isFunction(value);if(isFunction||!(l<=1||typeof value!=="string"||jQuery.support.checkClone||!rchecked.test(value))){return this.each(function(index){var self=set.eq(index);if(isFunction){args[0]=value.call(this,index,self.html());}
+self.domManip(args,callback,allowIntersection);});}
+if(l){fragment=jQuery.buildFragment(args,this[0].ownerDocument,false,!allowIntersection&&this);first=fragment.firstChild;if(fragment.childNodes.length===1){fragment=first;}
+if(first){scripts=jQuery.map(getAll(fragment,"script"),disableScript);hasScripts=scripts.length;for(;i<l;i++){node=fragment;if(i!==iNoClone){node=jQuery.clone(node,true,true);if(hasScripts){jQuery.merge(scripts,getAll(node,"script"));}}
+callback.call(this[i],node,i);}
+if(hasScripts){doc=scripts[scripts.length-1].ownerDocument;jQuery.map(scripts,restoreScript);for(i=0;i<hasScripts;i++){node=scripts[i];if(rscriptType.test(node.type||"")&&!data_priv.access(node,"globalEval")&&jQuery.contains(doc,node)){if(node.src){jQuery._evalUrl(node.src);}else{jQuery.globalEval(node.textContent.replace(rcleanScript,""));}}}}}}
+return this;}});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(name,original){jQuery.fn[name]=function(selector){var elems,ret=[],insert=jQuery(selector),last=insert.length-1,i=0;for(;i<=last;i++){elems=i===last?this:this.clone(true);jQuery(insert[i])[original](elems);core_push.apply(ret,elems.get());}
+return this.pushStack(ret);};});jQuery.extend({clone:function(elem,dataAndEvents,deepDataAndEvents){var i,l,srcElements,destElements,clone=elem.cloneNode(true),inPage=jQuery.contains(elem.ownerDocument,elem);if(!jQuery.support.noCloneChecked&&(elem.nodeType===1||elem.nodeType===11)&&!jQuery.isXMLDoc(elem)){destElements=getAll(clone);srcElements=getAll(elem);for(i=0,l=srcElements.length;i<l;i++){fixInput(srcElements[i],destElements[i]);}}
+if(dataAndEvents){if(deepDataAndEvents){srcElements=srcElements||getAll(elem);destElements=destElements||getAll(clone);for(i=0,l=srcElements.length;i<l;i++){cloneCopyEvent(srcElements[i],destElements[i]);}}else{cloneCopyEvent(elem,clone);}}
+destElements=getAll(clone,"script");if(destElements.length>0){setGlobalEval(destElements,!inPage&&getAll(elem,"script"));}
+return clone;},buildFragment:function(elems,context,scripts,selection){var elem,tmp,tag,wrap,contains,j,i=0,l=elems.length,fragment=context.createDocumentFragment(),nodes=[];for(;i<l;i++){elem=elems[i];if(elem||elem===0){if(jQuery.type(elem)==="object"){jQuery.merge(nodes,elem.nodeType?[elem]:elem);}else{if(!rhtml.test(elem)){nodes.push(context.createTextNode(elem));}else{tmp=tmp||fragment.appendChild(context.createElement("div"));tag=(rtagName.exec(elem)||["",""])[1].toLowerCase();wrap=wrapMap[tag]||wrapMap._default;tmp.innerHTML=wrap[1]+elem.replace(rxhtmlTag,"<$1></$2>")+wrap[2];j=wrap[0];while(j--){tmp=tmp.firstChild;}
+jQuery.merge(nodes,tmp.childNodes);tmp=fragment.firstChild;tmp.textContent="";}}}}
+fragment.textContent="";i=0;while(elem=nodes[i++]){if(selection&&jQuery.inArray(elem,selection)!==-1){continue;}
+contains=jQuery.contains(elem.ownerDocument,elem);tmp=getAll(fragment.appendChild(elem),"script");if(contains){setGlobalEval(tmp);}
+if(scripts){j=0;while(elem=tmp[j++]){if(rscriptType.test(elem.type||"")){scripts.push(elem);}}}}
+return fragment;},cleanData:function(elems){var data,elem,events,type,key,j,special=jQuery.event.special,i=0;for(;(elem=elems[i])!==undefined;i++){if(Data.accepts(elem)){key=elem[data_priv.expando];if(key&&(data=data_priv.cache[key])){events=Object.keys(data.events||{});if(events.length){for(j=0;(type=events[j])!==undefined;j++){if(special[type]){jQuery.event.remove(elem,type);}else{jQuery.removeEvent(elem,type,data.handle);}}}
+if(data_priv.cache[key]){delete data_priv.cache[key];}}}
+delete data_user.cache[elem[data_user.expando]];}},_evalUrl:function(url){return jQuery.ajax({url:url,type:"GET",dataType:"script",async:false,global:false,"throws":true});}});function manipulationTarget(elem,content){return jQuery.nodeName(elem,"table")&&jQuery.nodeName(content.nodeType===1?content:content.firstChild,"tr")?elem.getElementsByTagName("tbody")[0]||elem.appendChild(elem.ownerDocument.createElement("tbody")):elem;}
+function disableScript(elem){elem.type=(elem.getAttribute("type")!==null)+"/"+elem.type;return elem;}
+function restoreScript(elem){var match=rscriptTypeMasked.exec(elem.type);if(match){elem.type=match[1];}else{elem.removeAttribute("type");}
+return elem;}
+function setGlobalEval(elems,refElements){var l=elems.length,i=0;for(;i<l;i++){data_priv.set(elems[i],"globalEval",!refElements||data_priv.get(refElements[i],"globalEval"));}}
+function cloneCopyEvent(src,dest){var i,l,type,pdataOld,pdataCur,udataOld,udataCur,events;if(dest.nodeType!==1){return;}
+if(data_priv.hasData(src)){pdataOld=data_priv.access(src);pdataCur=data_priv.set(dest,pdataOld);events=pdataOld.events;if(events){delete pdataCur.handle;pdataCur.events={};for(type in events){for(i=0,l=events[type].length;i<l;i++){jQuery.event.add(dest,type,events[type][i]);}}}}
+if(data_user.hasData(src)){udataOld=data_user.access(src);udataCur=jQuery.extend({},udataOld);data_user.set(dest,udataCur);}}
+function getAll(context,tag){var ret=context.getElementsByTagName?context.getElementsByTagName(tag||"*"):context.querySelectorAll?context.querySelectorAll(tag||"*"):[];return tag===undefined||tag&&jQuery.nodeName(context,tag)?jQuery.merge([context],ret):ret;}
+function fixInput(src,dest){var nodeName=dest.nodeName.toLowerCase();if(nodeName==="input"&&manipulation_rcheckableType.test(src.type)){dest.checked=src.checked;}else{if(nodeName==="input"||nodeName==="textarea"){dest.defaultValue=src.defaultValue;}}}
+jQuery.fn.extend({wrapAll:function(html){var wrap;if(jQuery.isFunction(html)){return this.each(function(i){jQuery(this).wrapAll(html.call(this,i));});}
+if(this[0]){wrap=jQuery(html,this[0].ownerDocument).eq(0).clone(true);if(this[0].parentNode){wrap.insertBefore(this[0]);}
+wrap.map(function(){var elem=this;while(elem.firstElementChild){elem=elem.firstElementChild;}
+return elem;}).append(this);}
+return this;},wrapInner:function(html){if(jQuery.isFunction(html)){return this.each(function(i){jQuery(this).wrapInner(html.call(this,i));});}
+return this.each(function(){var self=jQuery(this),contents=self.contents();if(contents.length){contents.wrapAll(html);}else{self.append(html);}});},wrap:function(html){var isFunction=jQuery.isFunction(html);return this.each(function(i){jQuery(this).wrapAll(isFunction?html.call(this,i):html);});},unwrap:function(){return this.parent().each(function(){if(!jQuery.nodeName(this,"body")){jQuery(this).replaceWith(this.childNodes);}}).end();}});var curCSS,iframe,rdisplayswap=/^(none|table(?!-c[ea]).+)/,rmargin=/^margin/,rnumsplit=new RegExp("^("+core_pnum+")(.*)$","i"),rnumnonpx=new RegExp("^("+core_pnum+")(?!px)[a-z%]+$","i"),rrelNum=new RegExp("^([+-])=("+core_pnum+")","i"),elemdisplay={BODY:"block"},cssShow={position:"absolute",visibility:"hidden",display:"block"},cssNormalTransform={letterSpacing:0,fontWeight:400},cssExpand=["Top","Right","Bottom","Left"],cssPrefixes=["Webkit","O","Moz","ms"];function vendorPropName(style,name){if(name in style){return name;}
+var capName=name.charAt(0).toUpperCase()+name.slice(1),origName=name,i=cssPrefixes.length;while(i--){name=cssPrefixes[i]+capName;if(name in style){return name;}}
+return origName;}
+function isHidden(elem,el){elem=el||elem;return jQuery.css(elem,"display")==="none"||!jQuery.contains(elem.ownerDocument,elem);}
+function getStyles(elem){return window.getComputedStyle(elem,null);}
+function showHide(elements,show){var display,elem,hidden,values=[],index=0,length=elements.length;for(;index<length;index++){elem=elements[index];if(!elem.style){continue;}
+values[index]=data_priv.get(elem,"olddisplay");display=elem.style.display;if(show){if(!values[index]&&display==="none"){elem.style.display="";}
+if(elem.style.display===""&&isHidden(elem)){values[index]=data_priv.access(elem,"olddisplay",css_defaultDisplay(elem.nodeName));}}else{if(!values[index]){hidden=isHidden(elem);if(display&&display!=="none"||!hidden){data_priv.set(elem,"olddisplay",hidden?display:jQuery.css(elem,"display"));}}}}
+for(index=0;index<length;index++){elem=elements[index];if(!elem.style){continue;}
+if(!show||elem.style.display==="none"||elem.style.display===""){elem.style.display=show?values[index]||"":"none";}}
+return elements;}
+jQuery.fn.extend({css:function(name,value){return jQuery.access(this,function(elem,name,value){var styles,len,map={},i=0;if(jQuery.isArray(name)){styles=getStyles(elem);len=name.length;for(;i<len;i++){map[name[i]]=jQuery.css(elem,name[i],false,styles);}
+return map;}
+return value!==undefined?jQuery.style(elem,name,value):jQuery.css(elem,name);},name,value,arguments.length>1);},show:function(){return showHide(this,true);},hide:function(){return showHide(this);},toggle:function(state){var bool=typeof state==="boolean";return this.each(function(){if(bool?state:isHidden(this)){jQuery(this).show();}else{jQuery(this).hide();}});}});jQuery.extend({cssHooks:{opacity:{get:function(elem,computed){if(computed){var ret=curCSS(elem,"opacity");return ret===""?"1":ret;}}}},cssNumber:{"columnCount":true,"fillOpacity":true,"fontWeight":true,"lineHeight":true,"opacity":true,"orphans":true,"widows":true,"zIndex":true,"zoom":true},cssProps:{"float":"cssFloat"},style:function(elem,name,value,extra){if(!elem||elem.nodeType===3||elem.nodeType===8||!elem.style){return;}
+var ret,type,hooks,origName=jQuery.camelCase(name),style=elem.style;name=jQuery.cssProps[origName]||(jQuery.cssProps[origName]=vendorPropName(style,origName));hooks=jQuery.cssHooks[name]||jQuery.cssHooks[origName];if(value!==undefined){type=typeof value;if(type==="string"&&(ret=rrelNum.exec(value))){value=(ret[1]+1)*ret[2]+parseFloat(jQuery.css(elem,name));type="number";}
+if(value==null||type==="number"&&isNaN(value)){return;}
+if(type==="number"&&!jQuery.cssNumber[origName]){value+="px";}
+if(!jQuery.support.clearCloneStyle&&value===""&&name.indexOf("background")===0){style[name]="inherit";}
+if(!hooks||!("set"in hooks)||(value=hooks.set(elem,value,extra))!==undefined){style[name]=value;}}else{if(hooks&&"get"in hooks&&(ret=hooks.get(elem,false,extra))!==undefined){return ret;}
+return style[name];}},css:function(elem,name,extra,styles){var val,num,hooks,origName=jQuery.camelCase(name);name=jQuery.cssProps[origName]||(jQuery.cssProps[origName]=vendorPropName(elem.style,origName));hooks=jQuery.cssHooks[name]||jQuery.cssHooks[origName];if(hooks&&"get"in hooks){val=hooks.get(elem,true,extra);}
+if(val===undefined){val=curCSS(elem,name,styles);}
+if(val==="normal"&&name in cssNormalTransform){val=cssNormalTransform[name];}
+if(extra===""||extra){num=parseFloat(val);return extra===true||jQuery.isNumeric(num)?num||0:val;}
+return val;}});curCSS=function(elem,name,_computed){var width,minWidth,maxWidth,computed=_computed||getStyles(elem),ret=computed?computed.getPropertyValue(name)||computed[name]:undefined,style=elem.style;if(computed){if(ret===""&&!jQuery.contains(elem.ownerDocument,elem)){ret=jQuery.style(elem,name);}
+if(rnumnonpx.test(ret)&&rmargin.test(name)){width=style.width;minWidth=style.minWidth;maxWidth=style.maxWidth;style.minWidth=style.maxWidth=style.width=ret;ret=computed.width;style.width=width;style.minWidth=minWidth;style.maxWidth=maxWidth;}}
+return ret;};function setPositiveNumber(elem,value,subtract){var matches=rnumsplit.exec(value);return matches?Math.max(0,matches[1]-(subtract||0))+(matches[2]||"px"):value;}
+function augmentWidthOrHeight(elem,name,extra,isBorderBox,styles){var i=extra===(isBorderBox?"border":"content")?4:name==="width"?1:0,val=0;for(;i<4;i+=2){if(extra==="margin"){val+=jQuery.css(elem,extra+cssExpand[i],true,styles);}
+if(isBorderBox){if(extra==="content"){val-=jQuery.css(elem,"padding"+cssExpand[i],true,styles);}
+if(extra!=="margin"){val-=jQuery.css(elem,"border"+cssExpand[i]+"Width",true,styles);}}else{val+=jQuery.css(elem,"padding"+cssExpand[i],true,styles);if(extra!=="padding"){val+=jQuery.css(elem,"border"+cssExpand[i]+"Width",true,styles);}}}
+return val;}
+function getWidthOrHeight(elem,name,extra){var valueIsBorderBox=true,val=name==="width"?elem.offsetWidth:elem.offsetHeight,styles=getStyles(elem),isBorderBox=jQuery.support.boxSizing&&jQuery.css(elem,"boxSizing",false,styles)==="border-box";if(val<=0||val==null){val=curCSS(elem,name,styles);if(val<0||val==null){val=elem.style[name];}
+if(rnumnonpx.test(val)){return val;}
+valueIsBorderBox=isBorderBox&&(jQuery.support.boxSizingReliable||val===elem.style[name]);val=parseFloat(val)||0;}
+return val+augmentWidthOrHeight(elem,name,extra||(isBorderBox?"border":"content"),valueIsBorderBox,styles)+"px";}
+function css_defaultDisplay(nodeName){var doc=document,display=elemdisplay[nodeName];if(!display){display=actualDisplay(nodeName,doc);if(display==="none"||!display){iframe=(iframe||jQuery("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(doc.documentElement);doc=(iframe[0].contentWindow||iframe[0].contentDocument).document;doc.write("<!doctype html><html><body>");doc.close();display=actualDisplay(nodeName,doc);iframe.detach();}
+elemdisplay[nodeName]=display;}
+return display;}
+function actualDisplay(name,doc){var elem=jQuery(doc.createElement(name)).appendTo(doc.body),display=jQuery.css(elem[0],"display");elem.remove();return display;}
+jQuery.each(["height","width"],function(i,name){jQuery.cssHooks[name]={get:function(elem,computed,extra){if(computed){return elem.offsetWidth===0&&rdisplayswap.test(jQuery.css(elem,"display"))?jQuery.swap(elem,cssShow,function(){return getWidthOrHeight(elem,name,extra);}):getWidthOrHeight(elem,name,extra);}},set:function(elem,value,extra){var styles=extra&&getStyles(elem);return setPositiveNumber(elem,value,extra?augmentWidthOrHeight(elem,name,extra,jQuery.support.boxSizing&&jQuery.css(elem,"boxSizing",false,styles)==="border-box",styles):0);}};});jQuery(function(){if(!jQuery.support.reliableMarginRight){jQuery.cssHooks.marginRight={get:function(elem,computed){if(computed){return jQuery.swap(elem,{"display":"inline-block"},curCSS,[elem,"marginRight"]);}}};}
+if(!jQuery.support.pixelPosition&&jQuery.fn.position){jQuery.each(["top","left"],function(i,prop){jQuery.cssHooks[prop]={get:function(elem,computed){if(computed){computed=curCSS(elem,prop);return rnumnonpx.test(computed)?jQuery(elem).position()[prop]+"px":computed;}}};});}});if(jQuery.expr&&jQuery.expr.filters){jQuery.expr.filters.hidden=function(elem){return elem.offsetWidth<=0&&elem.offsetHeight<=0;};jQuery.expr.filters.visible=function(elem){return!jQuery.expr.filters.hidden(elem);};}
+jQuery.each({margin:"",padding:"",border:"Width"},function(prefix,suffix){jQuery.cssHooks[prefix+suffix]={expand:function(value){var i=0,expanded={},parts=typeof value==="string"?value.split(" "):[value];for(;i<4;i++){expanded[prefix+cssExpand[i]+suffix]=parts[i]||parts[i-2]||parts[0];}
+return expanded;}};if(!rmargin.test(prefix)){jQuery.cssHooks[prefix+suffix].set=setPositiveNumber;}});var r20=/%20/g,rbracket=/\[\]$/,rCRLF=/\r?\n/g,rsubmitterTypes=/^(?:submit|button|image|reset|file)$/i,rsubmittable=/^(?:input|select|textarea|keygen)/i;jQuery.fn.extend({serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){var elements=jQuery.prop(this,"elements");return elements?jQuery.makeArray(elements):this;}).filter(function(){var type=this.type;return this.name&&!jQuery(this).is(":disabled")&&rsubmittable.test(this.nodeName)&&!rsubmitterTypes.test(type)&&(this.checked||!manipulation_rcheckableType.test(type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:jQuery.isArray(val)?jQuery.map(val,function(val){return{name:elem.name,value:val.replace(rCRLF,"\r\n")};}):{name:elem.name,value:val.replace(rCRLF,"\r\n")};}).get();}});jQuery.param=function(a,traditional){var prefix,s=[],add=function(key,value){value=jQuery.isFunction(value)?value():value==null?"":value;s[s.length]=encodeURIComponent(key)+"="+encodeURIComponent(value);};if(traditional===undefined){traditional=jQuery.ajaxSettings&&jQuery.ajaxSettings.traditional;}
+if(jQuery.isArray(a)||a.jquery&&!jQuery.isPlainObject(a)){jQuery.each(a,function(){add(this.name,this.value);});}else{for(prefix in a){buildParams(prefix,a[prefix],traditional,add);}}
+return s.join("&").replace(r20,"+");};function buildParams(prefix,obj,traditional,add){var name;if(jQuery.isArray(obj)){jQuery.each(obj,function(i,v){if(traditional||rbracket.test(prefix)){add(prefix,v);}else{buildParams(prefix+"["+(typeof v==="object"?i:"")+"]",v,traditional,add);}});}else{if(!traditional&&jQuery.type(obj)==="object"){for(name in obj){buildParams(prefix+"["+name+"]",obj[name],traditional,add);}}else{add(prefix,obj);}}}
+jQuery.each(("blur focus focusin focusout load resize scroll unload click dblclick "+"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave "+"change select submit keydown keypress keyup error contextmenu").split(" "),function(i,name){jQuery.fn[name]=function(data,fn){return arguments.length>0?this.on(name,null,data,fn):this.trigger(name);};});jQuery.fn.extend({hover:function(fnOver,fnOut){return this.mouseenter(fnOver).mouseleave(fnOut||fnOver);},bind:function(types,data,fn){return this.on(types,null,data,fn);},unbind:function(types,fn){return this.off(types,null,fn);},delegate:function(selector,types,data,fn){return this.on(types,selector,data,fn);},undelegate:function(selector,types,fn){return arguments.length===1?this.off(selector,"**"):this.off(types,selector||"**",fn);}});var ajaxLocParts,ajaxLocation,ajax_nonce=jQuery.now(),ajax_rquery=/\?/,rhash=/#.*$/,rts=/([?&])_=[^&]*/,rheaders=/^(.*?):[ \t]*([^\r\n]*)$/mg,rlocalProtocol=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,rnoContent=/^(?:GET|HEAD)$/,rprotocol=/^\/\//,rurl=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,_load=jQuery.fn.load,prefilters={},transports={},allTypes="*/".concat("*");try{ajaxLocation=location.href;}catch(e){ajaxLocation=document.createElement("a");ajaxLocation.href="";ajaxLocation=ajaxLocation.href;}
+ajaxLocParts=rurl.exec(ajaxLocation.toLowerCase())||[];function addToPrefiltersOrTransports(structure){return function(dataTypeExpression,func){if(typeof dataTypeExpression!=="string"){func=dataTypeExpression;dataTypeExpression="*";}
+var dataType,i=0,dataTypes=dataTypeExpression.toLowerCase().match(core_rnotwhite)||[];if(jQuery.isFunction(func)){while(dataType=dataTypes[i++]){if(dataType[0]==="+"){dataType=dataType.slice(1)||"*";(structure[dataType]=structure[dataType]||[]).unshift(func);}else{(structure[dataType]=structure[dataType]||[]).push(func);}}}};}
+function inspectPrefiltersOrTransports(structure,options,originalOptions,jqXHR){var inspected={},seekingTransport=structure===transports;function inspect(dataType){var selected;inspected[dataType]=true;jQuery.each(structure[dataType]||[],function(_,prefilterOrFactory){var dataTypeOrTransport=prefilterOrFactory(options,originalOptions,jqXHR);if(typeof dataTypeOrTransport==="string"&&!seekingTransport&&!inspected[dataTypeOrTransport]){options.dataTypes.unshift(dataTypeOrTransport);inspect(dataTypeOrTransport);return false;}else{if(seekingTransport){return!(selected=dataTypeOrTransport);}}});return selected;}
+return inspect(options.dataTypes[0])||!inspected["*"]&&inspect("*");}
+function ajaxExtend(target,src){var key,deep,flatOptions=jQuery.ajaxSettings.flatOptions||{};for(key in src){if(src[key]!==undefined){(flatOptions[key]?target:deep||(deep={}))[key]=src[key];}}
+if(deep){jQuery.extend(true,target,deep);}
+return target;}
+jQuery.fn.load=function(url,params,callback){if(typeof url!=="string"&&_load){return _load.apply(this,arguments);}
+var selector,type,response,self=this,off=url.indexOf(" ");if(off>=0){selector=url.slice(off);url=url.slice(0,off);}
+if(jQuery.isFunction(params)){callback=params;params=undefined;}else{if(params&&typeof params==="object"){type="POST";}}
+if(self.length>0){jQuery.ajax({url:url,type:type,dataType:"html",data:params}).done(function(responseText){response=arguments;self.html(selector?jQuery("<div>").append(jQuery.parseHTML(responseText)).find(selector):responseText);}).complete(callback&&function(jqXHR,status){self.each(callback,response||[jqXHR.responseText,status,jqXHR]);});}
+return this;};jQuery.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(i,type){jQuery.fn[type]=function(fn){return this.on(type,fn);};});jQuery.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ajaxLocation,type:"GET",isLocal:rlocalProtocol.test(ajaxLocParts[1]),global:true,processData:true,async:true,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":allTypes,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":true,"text json":jQuery.parseJSON,"text xml":jQuery.parseXML},flatOptions:{url:true,context:true}},ajaxSetup:function(target,settings){return settings?ajaxExtend(ajaxExtend(target,jQuery.ajaxSettings),settings):ajaxExtend(jQuery.ajaxSettings,target);},ajaxPrefilter:addToPrefiltersOrTransports(prefilters),ajaxTransport:addToPrefiltersOrTransports(transports),ajax:function(url,options){if(typeof url==="object"){options=url;url=undefined;}
+options=options||{};var transport,cacheURL,responseHeadersString,responseHeaders,timeoutTimer,parts,fireGlobals,i,s=jQuery.ajaxSetup({},options),callbackContext=s.context||s,globalEventContext=s.context&&(callbackContext.nodeType||callbackContext.jquery)?jQuery(callbackContext):jQuery.event,deferred=jQuery.Deferred(),completeDeferred=jQuery.Callbacks("once memory"),statusCode=s.statusCode||{},requestHeaders={},requestHeadersNames={},state=0,strAbort="canceled",jqXHR={readyState:0,getResponseHeader:function(key){var match;if(state===2){if(!responseHeaders){responseHeaders={};while(match=rheaders.exec(responseHeadersString)){responseHeaders[match[1].toLowerCase()]=match[2];}}
+match=responseHeaders[key.toLowerCase()];}
+return match==null?null:match;},getAllResponseHeaders:function(){return state===2?responseHeadersString:null;},setRequestHeader:function(name,value){var lname=name.toLowerCase();if(!state){name=requestHeadersNames[lname]=requestHeadersNames[lname]||name;requestHeaders[name]=value;}
+return this;},overrideMimeType:function(type){if(!state){s.mimeType=type;}
+return this;},statusCode:function(map){var code;if(map){if(state<2){for(code in map){statusCode[code]=[statusCode[code],map[code]];}}else{jqXHR.always(map[jqXHR.status]);}}
+return this;},abort:function(statusText){var finalText=statusText||strAbort;if(transport){transport.abort(finalText);}
+done(0,finalText);return this;}};deferred.promise(jqXHR).complete=completeDeferred.add;jqXHR.success=jqXHR.done;jqXHR.error=jqXHR.fail;s.url=((url||s.url||ajaxLocation)+"").replace(rhash,"").replace(rprotocol,ajaxLocParts[1]+"//");s.type=options.method||options.type||s.method||s.type;s.dataTypes=jQuery.trim(s.dataType||"*").toLowerCase().match(core_rnotwhite)||[""];if(s.crossDomain==null){parts=rurl.exec(s.url.toLowerCase());s.crossDomain=!!(parts&&(parts[1]!==ajaxLocParts[1]||parts[2]!==ajaxLocParts[2]||(parts[3]||(parts[1]==="http:"?"80":"443"))!==(ajaxLocParts[3]||(ajaxLocParts[1]==="http:"?"80":"443"))));}
+if(s.data&&s.processData&&typeof s.data!=="string"){s.data=jQuery.param(s.data,s.traditional);}
+inspectPrefiltersOrTransports(prefilters,s,options,jqXHR);if(state===2){return jqXHR;}
+fireGlobals=s.global;if(fireGlobals&&jQuery.active++===0){jQuery.event.trigger("ajaxStart");}
+s.type=s.type.toUpperCase();s.hasContent=!rnoContent.test(s.type);cacheURL=s.url;if(!s.hasContent){if(s.data){cacheURL=s.url+=(ajax_rquery.test(cacheURL)?"&":"?")+s.data;delete s.data;}
+if(s.cache===false){s.url=rts.test(cacheURL)?cacheURL.replace(rts,"$1_="+ajax_nonce++):cacheURL+(ajax_rquery.test(cacheURL)?"&":"?")+"_="+ajax_nonce++;}}
+if(s.ifModified){if(jQuery.lastModified[cacheURL]){jqXHR.setRequestHeader("If-Modified-Since",jQuery.lastModified[cacheURL]);}
+if(jQuery.etag[cacheURL]){jqXHR.setRequestHeader("If-None-Match",jQuery.etag[cacheURL]);}}
+if(s.data&&s.hasContent&&s.contentType!==false||options.contentType){jqXHR.setRequestHeader("Content-Type",s.contentType);}
+jqXHR.setRequestHeader("Accept",s.dataTypes[0]&&s.accepts[s.dataTypes[0]]?s.accepts[s.dataTypes[0]]+(s.dataTypes[0]!=="*"?", "+allTypes+"; q=0.01":""):s.accepts["*"]);for(i in s.headers){jqXHR.setRequestHeader(i,s.headers[i]);}
+if(s.beforeSend&&(s.beforeSend.call(callbackContext,jqXHR,s)===false||state===2)){return jqXHR.abort();}
+strAbort="abort";for(i in{success:1,error:1,complete:1}){jqXHR[i](s[i]);}
+transport=inspectPrefiltersOrTransports(transports,s,options,jqXHR);if(!transport){done(-1,"No Transport");}else{jqXHR.readyState=1;if(fireGlobals){globalEventContext.trigger("ajaxSend",[jqXHR,s]);}
+if(s.async&&s.timeout>0){timeoutTimer=setTimeout(function(){jqXHR.abort("timeout");},s.timeout);}
+try{state=1;transport.send(requestHeaders,done);}catch(e$2){if(state<2){done(-1,e$2);}else{throw e$2;}}}
+function done(status,nativeStatusText,responses,headers){var isSuccess,success,error,response,modified,statusText=nativeStatusText;if(state===2){return;}
+state=2;if(timeoutTimer){clearTimeout(timeoutTimer);}
+transport=undefined;responseHeadersString=headers||"";jqXHR.readyState=status>0?4:0;isSuccess=status>=200&&status<300||status===304;if(responses){response=ajaxHandleResponses(s,jqXHR,responses);}
+response=ajaxConvert(s,response,jqXHR,isSuccess);if(isSuccess){if(s.ifModified){modified=jqXHR.getResponseHeader("Last-Modified");if(modified){jQuery.lastModified[cacheURL]=modified;}
+modified=jqXHR.getResponseHeader("etag");if(modified){jQuery.etag[cacheURL]=modified;}}
+if(status===204||s.type==="HEAD"){statusText="nocontent";}else{if(status===304){statusText="notmodified";}else{statusText=response.state;success=response.data;error=response.error;isSuccess=!error;}}}else{error=statusText;if(status||!statusText){statusText="error";if(status<0){status=0;}}}
+jqXHR.status=status;jqXHR.statusText=(nativeStatusText||statusText)+"";if(isSuccess){deferred.resolveWith(callbackContext,[success,statusText,jqXHR]);}else{deferred.rejectWith(callbackContext,[jqXHR,statusText,error]);}
+jqXHR.statusCode(statusCode);statusCode=undefined;if(fireGlobals){globalEventContext.trigger(isSuccess?"ajaxSuccess":"ajaxError",[jqXHR,s,isSuccess?success:error]);}
+completeDeferred.fireWith(callbackContext,[jqXHR,statusText]);if(fireGlobals){globalEventContext.trigger("ajaxComplete",[jqXHR,s]);if(!--jQuery.active){jQuery.event.trigger("ajaxStop");}}}
+return jqXHR;},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},getScript:function(url,callback){return jQuery.get(url,undefined,callback,"script");}});jQuery.each(["get","post"],function(i,method){jQuery[method]=function(url,data,callback,type){if(jQuery.isFunction(data)){type=type||callback;callback=data;data=undefined;}
+return jQuery.ajax({url:url,type:method,dataType:type,data:data,success:callback});};});function ajaxHandleResponses(s,jqXHR,responses){var ct,type,finalDataType,firstDataType,contents=s.contents,dataTypes=s.dataTypes;while(dataTypes[0]==="*"){dataTypes.shift();if(ct===undefined){ct=s.mimeType||jqXHR.getResponseHeader("Content-Type");}}
+if(ct){for(type in contents){if(contents[type]&&contents[type].test(ct)){dataTypes.unshift(type);break;}}}
+if(dataTypes[0]in responses){finalDataType=dataTypes[0];}else{for(type in responses){if(!dataTypes[0]||s.converters[type+" "+dataTypes[0]]){finalDataType=type;break;}
+if(!firstDataType){firstDataType=type;}}
+finalDataType=finalDataType||firstDataType;}
+if(finalDataType){if(finalDataType!==dataTypes[0]){dataTypes.unshift(finalDataType);}
+return responses[finalDataType];}}
+function ajaxConvert(s,response,jqXHR,isSuccess){var conv2,current,conv,tmp,prev,converters={},dataTypes=s.dataTypes.slice();if(dataTypes[1]){for(conv in s.converters){converters[conv.toLowerCase()]=s.converters[conv];}}
+current=dataTypes.shift();while(current){if(s.responseFields[current]){jqXHR[s.responseFields[current]]=response;}
+if(!prev&&isSuccess&&s.dataFilter){response=s.dataFilter(response,s.dataType);}
+prev=current;current=dataTypes.shift();if(current){if(current==="*"){current=prev;}else{if(prev!=="*"&&prev!==current){conv=converters[prev+" "+current]||converters["* "+current];if(!conv){for(conv2 in converters){tmp=conv2.split(" ");if(tmp[1]===current){conv=converters[prev+" "+tmp[0]]||converters["* "+tmp[0]];if(conv){if(conv===true){conv=converters[conv2];}else{if(converters[conv2]!==true){current=tmp[0];dataTypes.unshift(tmp[1]);}}
+break;}}}}
+if(conv!==true){if(conv&&s["throws"]){response=conv(response);}else{try{response=conv(response);}catch(e$3){return{state:"parsererror",error:conv?e$3:"No conversion from "+prev+" to "+current};}}}}}}}
+return{state:"success",data:response};}
+jQuery.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(text){jQuery.globalEval(text);return text;}}});jQuery.ajaxPrefilter("script",function(s){if(s.cache===undefined){s.cache=false;}
+if(s.crossDomain){s.type="GET";}});jQuery.ajaxTransport("script",function(s){if(s.crossDomain){var script,callback;return{send:function(_,complete){script=jQuery("<script>").prop({async:true,charset:s.scriptCharset,src:s.url}).on("load error",callback=function(evt){script.remove();callback=null;if(evt){complete(evt.type==="error"?404:200,evt.type);}});document.head.appendChild(script[0]);},abort:function(){if(callback){callback();}}};}});var oldCallbacks=[],rjsonp=/(=)\?(?=&|$)|\?\?/;jQuery.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var callback=oldCallbacks.pop()||jQuery.expando+"_"+ajax_nonce++;this[callback]=true;return callback;}});jQuery.ajaxPrefilter("json jsonp",function(s,originalSettings,jqXHR){var callbackName,overwritten,responseContainer,jsonProp=s.jsonp!==false&&(rjsonp.test(s.url)?"url":typeof s.data==="string"&&!(s.contentType||"").indexOf("application/x-www-form-urlencoded")&&rjsonp.test(s.data)&&"data");if(jsonProp||s.dataTypes[0]==="jsonp"){callbackName=s.jsonpCallback=jQuery.isFunction(s.jsonpCallback)?s.jsonpCallback():s.jsonpCallback;if(jsonProp){s[jsonProp]=s[jsonProp].replace(rjsonp,"$1"+callbackName);}else{if(s.jsonp!==false){s.url+=(ajax_rquery.test(s.url)?"&":"?")+s.jsonp+"="+callbackName;}}
+s.converters["script json"]=function(){if(!responseContainer){jQuery.error(callbackName+" was not called");}
+return responseContainer[0];};s.dataTypes[0]="json";overwritten=window[callbackName];window[callbackName]=function(){responseContainer=arguments;};jqXHR.always(function(){window[callbackName]=overwritten;if(s[callbackName]){s.jsonpCallback=originalSettings.jsonpCallback;oldCallbacks.push(callbackName);}
+if(responseContainer&&jQuery.isFunction(overwritten)){overwritten(responseContainer[0]);}
+responseContainer=overwritten=undefined;});return"script";}});jQuery.ajaxSettings.xhr=function(){try{return new XMLHttpRequest;}catch(e$4){}};var xhrSupported=jQuery.ajaxSettings.xhr(),xhrSuccessStatus={0:200,1223:204},xhrId=0,xhrCallbacks={};if(window.ActiveXObject){jQuery(window).on("unload",function(){for(var key in xhrCallbacks){xhrCallbacks[key]();}
+xhrCallbacks=undefined;});}
+jQuery.support.cors=!!xhrSupported&&"withCredentials"in xhrSupported;jQuery.support.ajax=xhrSupported=!!xhrSupported;jQuery.ajaxTransport(function(options){var callback;if(jQuery.support.cors||xhrSupported&&!options.crossDomain){return{send:function(headers,complete){var i,id,xhr=options.xhr();xhr.open(options.type,options.url,options.async,options.username,options.password);if(options.xhrFields){for(i in options.xhrFields){xhr[i]=options.xhrFields[i];}}
+if(options.mimeType&&xhr.overrideMimeType){xhr.overrideMimeType(options.mimeType);}
+if(!options.crossDomain&&!headers["X-Requested-With"]){headers["X-Requested-With"]="XMLHttpRequest";}
+for(i in headers){xhr.setRequestHeader(i,headers[i]);}
+callback=function(type){return function(){if(callback){delete xhrCallbacks[id];callback=xhr.onload=xhr.onerror=null;if(type==="abort"){xhr.abort();}else{if(type==="error"){complete(xhr.status||404,xhr.statusText);}else{complete(xhrSuccessStatus[xhr.status]||xhr.status,xhr.statusText,typeof xhr.responseText==="string"?{text:xhr.responseText}:undefined,xhr.getAllResponseHeaders());}}}};};xhr.onload=callback();xhr.onerror=callback("error");callback=xhrCallbacks[id=xhrId++]=callback("abort");xhr.send(options.hasContent&&options.data||null);},abort:function(){if(callback){callback();}}};}});var fxNow,timerId,rfxtypes=/^(?:toggle|show|hide)$/,rfxnum=new RegExp("^(?:([+-])=|)("+core_pnum+")([a-z%]*)$","i"),rrun=/queueHooks$/,animationPrefilters=[defaultPrefilter],tweeners={"*":[function(prop,value){var tween=this.createTween(prop,value),target=tween.cur(),parts=rfxnum.exec(value),unit=parts&&parts[3]||(jQuery.cssNumber[prop]?"":"px"),start=(jQuery.cssNumber[prop]||unit!=="px"&&+target)&&rfxnum.exec(jQuery.css(tween.elem,prop)),scale=1,maxIterations=20;if(start&&start[3]!==unit){unit=unit||start[3];parts=parts||[];start=+target||1;do{scale=scale||".5";start=start / scale;jQuery.style(tween.elem,prop,start+unit);}while(scale!==(scale=tween.cur()/ target)&&scale!==1&&--maxIterations);}
+if(parts){start=tween.start=+start||+target||0;tween.unit=unit;tween.end=parts[1]?start+(parts[1]+1)*parts[2]:+parts[2];}
+return tween;}]};function createFxNow(){setTimeout(function(){fxNow=undefined;});return fxNow=jQuery.now();}
+function createTween(value,prop,animation){var tween,collection=(tweeners[prop]||[]).concat(tweeners["*"]),index=0,length=collection.length;for(;index<length;index++){if(tween=collection[index].call(animation,prop,value)){return tween;}}}
+function Animation(elem,properties,options){var result,stopped,index=0,length=animationPrefilters.length,deferred=jQuery.Deferred().always(function(){delete tick.elem;}),tick=function(){if(stopped){return false;}
+var currentTime=fxNow||createFxNow(),remaining=Math.max(0,animation.startTime+animation.duration-currentTime),temp=remaining / animation.duration||0,percent=1-temp,index=0,length=animation.tweens.length;for(;index<length;index++){animation.tweens[index].run(percent);}
+deferred.notifyWith(elem,[animation,percent,remaining]);if(percent<1&&length){return remaining;}else{deferred.resolveWith(elem,[animation]);return false;}},animation=deferred.promise({elem:elem,props:jQuery.extend({},properties),opts:jQuery.extend(true,{specialEasing:{}},options),originalProperties:properties,originalOptions:options,startTime:fxNow||createFxNow(),duration:options.duration,tweens:[],createTween:function(prop,end){var tween=jQuery.Tween(elem,animation.opts,prop,end,animation.opts.specialEasing[prop]||animation.opts.easing);animation.tweens.push(tween);return tween;},stop:function(gotoEnd){var index=0,length=gotoEnd?animation.tweens.length:0;if(stopped){return this;}
+stopped=true;for(;index<length;index++){animation.tweens[index].run(1);}
+if(gotoEnd){deferred.resolveWith(elem,[animation,gotoEnd]);}else{deferred.rejectWith(elem,[animation,gotoEnd]);}
+return this;}}),props=animation.props;propFilter(props,animation.opts.specialEasing);for(;index<length;index++){result=animationPrefilters[index].call(animation,elem,props,animation.opts);if(result){return result;}}
+jQuery.map(props,createTween,animation);if(jQuery.isFunction(animation.opts.start)){animation.opts.start.call(elem,animation);}
+jQuery.fx.timer(jQuery.extend(tick,{elem:elem,anim:animation,queue:animation.opts.queue}));return animation.progress(animation.opts.progress).done(animation.opts.done,animation.opts.complete).fail(animation.opts.fail).always(animation.opts.always);}
+function propFilter(props,specialEasing){var index,name,easing,value,hooks;for(index in props){name=jQuery.camelCase(index);easing=specialEasing[name];value=props[index];if(jQuery.isArray(value)){easing=value[1];value=props[index]=value[0];}
+if(index!==name){props[name]=value;delete props[index];}
+hooks=jQuery.cssHooks[name];if(hooks&&"expand"in hooks){value=hooks.expand(value);delete props[name];for(index in value){if(!(index in props)){props[index]=value[index];specialEasing[index]=easing;}}}else{specialEasing[name]=easing;}}}
+jQuery.Animation=jQuery.extend(Animation,{tweener:function(props,callback){if(jQuery.isFunction(props)){callback=props;props=["*"];}else{props=props.split(" ");}
+var prop,index=0,length=props.length;for(;index<length;index++){prop=props[index];tweeners[prop]=tweeners[prop]||[];tweeners[prop].unshift(callback);}},prefilter:function(callback,prepend){if(prepend){animationPrefilters.unshift(callback);}else{animationPrefilters.push(callback);}}});function defaultPrefilter(elem,props,opts){var prop,value,toggle,tween,hooks,oldfire,anim=this,orig={},style=elem.style,hidden=elem.nodeType&&isHidden(elem),dataShow=data_priv.get(elem,"fxshow");if(!opts.queue){hooks=jQuery._queueHooks(elem,"fx");if(hooks.unqueued==null){hooks.unqueued=0;oldfire=hooks.empty.fire;hooks.empty.fire=function(){if(!hooks.unqueued){oldfire();}};}
+hooks.unqueued++;anim.always(function(){anim.always(function(){hooks.unqueued--;if(!jQuery.queue(elem,"fx").length){hooks.empty.fire();}});});}
+if(elem.nodeType===1&&("height"in props||"width"in props)){opts.overflow=[style.overflow,style.overflowX,style.overflowY];if(jQuery.css(elem,"display")==="inline"&&jQuery.css(elem,"float")==="none"){style.display="inline-block";}}
+if(opts.overflow){style.overflow="hidden";anim.always(function(){style.overflow=opts.overflow[0];style.overflowX=opts.overflow[1];style.overflowY=opts.overflow[2];});}
+for(prop in props){value=props[prop];if(rfxtypes.exec(value)){delete props[prop];toggle=toggle||value==="toggle";if(value===(hidden?"hide":"show")){if(value==="show"&&dataShow&&dataShow[prop]!==undefined){hidden=true;}else{continue;}}
+orig[prop]=dataShow&&dataShow[prop]||jQuery.style(elem,prop);}}
+if(!jQuery.isEmptyObject(orig)){if(dataShow){if("hidden"in dataShow){hidden=dataShow.hidden;}}else{dataShow=data_priv.access(elem,"fxshow",{});}
+if(toggle){dataShow.hidden=!hidden;}
+if(hidden){jQuery(elem).show();}else{anim.done(function(){jQuery(elem).hide();});}
+anim.done(function(){var prop;data_priv.remove(elem,"fxshow");for(prop in orig){jQuery.style(elem,prop,orig[prop]);}});for(prop in orig){tween=createTween(hidden?dataShow[prop]:0,prop,anim);if(!(prop in dataShow)){dataShow[prop]=tween.start;if(hidden){tween.end=tween.start;tween.start=prop==="width"||prop==="height"?1:0;}}}}}
+function Tween(elem,options,prop,end,easing){return new Tween.prototype.init(elem,options,prop,end,easing);}
+jQuery.Tween=Tween;Tween.prototype={constructor:Tween,init:function(elem,options,prop,end,easing,unit){this.elem=elem;this.prop=prop;this.easing=easing||"swing";this.options=options;this.start=this.now=this.cur();this.end=end;this.unit=unit||(jQuery.cssNumber[prop]?"":"px");},cur:function(){var hooks=Tween.propHooks[this.prop];return hooks&&hooks.get?hooks.get(this):Tween.propHooks._default.get(this);},run:function(percent){var eased,hooks=Tween.propHooks[this.prop];if(this.options.duration){this.pos=eased=jQuery.easing[this.easing](percent,this.options.duration*percent,0,1,this.options.duration);}else{this.pos=eased=percent;}
+this.now=(this.end-this.start)*eased+this.start;if(this.options.step){this.options.step.call(this.elem,this.now,this);}
+if(hooks&&hooks.set){hooks.set(this);}else{Tween.propHooks._default.set(this);}
+return this;}};Tween.prototype.init.prototype=Tween.prototype;Tween.propHooks={_default:{get:function(tween){var result;if(tween.elem[tween.prop]!=null&&(!tween.elem.style||tween.elem.style[tween.prop]==null)){return tween.elem[tween.prop];}
+result=jQuery.css(tween.elem,tween.prop,"");return!result||result==="auto"?0:result;},set:function(tween){if(jQuery.fx.step[tween.prop]){jQuery.fx.step[tween.prop](tween);}else{if(tween.elem.style&&(tween.elem.style[jQuery.cssProps[tween.prop]]!=null||jQuery.cssHooks[tween.prop])){jQuery.style(tween.elem,tween.prop,tween.now+tween.unit);}else{tween.elem[tween.prop]=tween.now;}}}}};Tween.propHooks.scrollTop=Tween.propHooks.scrollLeft={set:function(tween){if(tween.elem.nodeType&&tween.elem.parentNode){tween.elem[tween.prop]=tween.now;}}};jQuery.each(["toggle","show","hide"],function(i,name){var cssFn=jQuery.fn[name];jQuery.fn[name]=function(speed,easing,callback){return speed==null||typeof speed==="boolean"?cssFn.apply(this,arguments):this.animate(genFx(name,true),speed,easing,callback);};});jQuery.fn.extend({fadeTo:function(speed,to,easing,callback){return this.filter(isHidden).css("opacity",0).show().end().animate({opacity:to},speed,easing,callback);},animate:function(prop,speed,easing,callback){var empty=jQuery.isEmptyObject(prop),optall=jQuery.speed(speed,easing,callback),doAnimation=function(){var anim=Animation(this,jQuery.extend({},prop),optall);if(empty||data_priv.get(this,"finish")){anim.stop(true);}};doAnimation.finish=doAnimation;return empty||optall.queue===false?this.each(doAnimation):this.queue(optall.queue,doAnimation);},stop:function(type,clearQueue,gotoEnd){var stopQueue=function(hooks){var stop=hooks.stop;delete hooks.stop;stop(gotoEnd);};if(typeof type!=="string"){gotoEnd=clearQueue;clearQueue=type;type=undefined;}
+if(clearQueue&&type!==false){this.queue(type||"fx",[]);}
+return this.each(function(){var dequeue=true,index=type!=null&&type+"queueHooks",timers=jQuery.timers,data=data_priv.get(this);if(index){if(data[index]&&data[index].stop){stopQueue(data[index]);}}else{for(index in data){if(data[index]&&data[index].stop&&rrun.test(index)){stopQueue(data[index]);}}}
+for(index=timers.length;index--;){if(timers[index].elem===this&&(type==null||timers[index].queue===type)){timers[index].anim.stop(gotoEnd);dequeue=false;timers.splice(index,1);}}
+if(dequeue||!gotoEnd){jQuery.dequeue(this,type);}});},finish:function(type){if(type!==false){type=type||"fx";}
+return this.each(function(){var index,data=data_priv.get(this),queue=data[type+"queue"],hooks=data[type+"queueHooks"],timers=jQuery.timers,length=queue?queue.length:0;data.finish=true;jQuery.queue(this,type,[]);if(hooks&&hooks.stop){hooks.stop.call(this,true);}
+for(index=timers.length;index--;){if(timers[index].elem===this&&timers[index].queue===type){timers[index].anim.stop(true);timers.splice(index,1);}}
+for(index=0;index<length;index++){if(queue[index]&&queue[index].finish){queue[index].finish.call(this);}}
+delete data.finish;});}});function genFx(type,includeWidth){var which,attrs={height:type},i=0;includeWidth=includeWidth?1:0;for(;i<4;i+=2-includeWidth){which=cssExpand[i];attrs["margin"+which]=attrs["padding"+which]=type;}
+if(includeWidth){attrs.opacity=attrs.width=type;}
+return attrs;}
+jQuery.each({slideDown:genFx("show"),slideUp:genFx("hide"),slideToggle:genFx("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(name,props){jQuery.fn[name]=function(speed,easing,callback){return this.animate(props,speed,easing,callback);};});jQuery.speed=function(speed,easing,fn){var opt=speed&&typeof speed==="object"?jQuery.extend({},speed):{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&!jQuery.isFunction(easing)&&easing};opt.duration=jQuery.fx.off?0:typeof opt.duration==="number"?opt.duration:opt.duration in jQuery.fx.speeds?jQuery.fx.speeds[opt.duration]:jQuery.fx.speeds._default;if(opt.queue==null||opt.queue===true){opt.queue="fx";}
+opt.old=opt.complete;opt.complete=function(){if(jQuery.isFunction(opt.old)){opt.old.call(this);}
+if(opt.queue){jQuery.dequeue(this,opt.queue);}};return opt;};jQuery.easing={linear:function(p){return p;},swing:function(p){return.5-Math.cos(p*Math.PI)/ 2;}};jQuery.timers=[];jQuery.fx=Tween.prototype.init;jQuery.fx.tick=function(){var timer,timers=jQuery.timers,i=0;fxNow=jQuery.now();for(;i<timers.length;i++){timer=timers[i];if(!timer()&&timers[i]===timer){timers.splice(i--,1);}}
+if(!timers.length){jQuery.fx.stop();}
+fxNow=undefined;};jQuery.fx.timer=function(timer){if(timer()&&jQuery.timers.push(timer)){jQuery.fx.start();}};jQuery.fx.interval=13;jQuery.fx.start=function(){if(!timerId){timerId=setInterval(jQuery.fx.tick,jQuery.fx.interval);}};jQuery.fx.stop=function(){clearInterval(timerId);timerId=null;};jQuery.fx.speeds={slow:600,fast:200,_default:400};jQuery.fx.step={};if(jQuery.expr&&jQuery.expr.filters){jQuery.expr.filters.animated=function(elem){return jQuery.grep(jQuery.timers,function(fn){return elem===fn.elem;}).length;};}
+jQuery.fn.offset=function(options){if(arguments.length){return options===undefined?this:this.each(function(i){jQuery.offset.setOffset(this,options,i);});}
+var docElem,win,elem=this[0],box={top:0,left:0},doc=elem&&elem.ownerDocument;if(!doc){return;}
+docElem=doc.documentElement;if(!jQuery.contains(docElem,elem)){return box;}
+if(typeof elem.getBoundingClientRect!==core_strundefined){box=elem.getBoundingClientRect();}
+win=getWindow(doc);return{top:box.top+win.pageYOffset-docElem.clientTop,left:box.left+win.pageXOffset-docElem.clientLeft};};jQuery.offset={setOffset:function(elem,options,i){var curPosition,curLeft,curCSSTop,curTop,curOffset,curCSSLeft,calculatePosition,position=jQuery.css(elem,"position"),curElem=jQuery(elem),props={};if(position==="static"){elem.style.position="relative";}
+curOffset=curElem.offset();curCSSTop=jQuery.css(elem,"top");curCSSLeft=jQuery.css(elem,"left");calculatePosition=(position==="absolute"||position==="fixed")&&(curCSSTop+curCSSLeft).indexOf("auto")>-1;if(calculatePosition){curPosition=curElem.position();curTop=curPosition.top;curLeft=curPosition.left;}else{curTop=parseFloat(curCSSTop)||0;curLeft=parseFloat(curCSSLeft)||0;}
+if(jQuery.isFunction(options)){options=options.call(elem,i,curOffset);}
+if(options.top!=null){props.top=options.top-curOffset.top+curTop;}
+if(options.left!=null){props.left=options.left-curOffset.left+curLeft;}
+if("using"in options){options.using.call(elem,props);}else{curElem.css(props);}}};jQuery.fn.extend({position:function(){if(!this[0]){return;}
+var offsetParent,offset,elem=this[0],parentOffset={top:0,left:0};if(jQuery.css(elem,"position")==="fixed"){offset=elem.getBoundingClientRect();}else{offsetParent=this.offsetParent();offset=this.offset();if(!jQuery.nodeName(offsetParent[0],"html")){parentOffset=offsetParent.offset();}
+parentOffset.top+=jQuery.css(offsetParent[0],"borderTopWidth",true);parentOffset.left+=jQuery.css(offsetParent[0],"borderLeftWidth",true);}
+return{top:offset.top-parentOffset.top-jQuery.css(elem,"marginTop",true),left:offset.left-parentOffset.left-jQuery.css(elem,"marginLeft",true)};},offsetParent:function(){return this.map(function(){var offsetParent=this.offsetParent||docElem;while(offsetParent&&(!jQuery.nodeName(offsetParent,"html")&&jQuery.css(offsetParent,"position")==="static")){offsetParent=offsetParent.offsetParent;}
+return offsetParent||docElem;});}});jQuery.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(method,prop){var top="pageYOffset"===prop;jQuery.fn[method]=function(val){return jQuery.access(this,function(elem,method,val){var win=getWindow(elem);if(val===undefined){return win?win[prop]:elem[method];}
+if(win){win.scrollTo(!top?val:window.pageXOffset,top?val:window.pageYOffset);}else{elem[method]=val;}},method,val,arguments.length,null);};});function getWindow(elem){return jQuery.isWindow(elem)?elem:elem.nodeType===9&&elem.defaultView;}
+jQuery.each({Height:"height",Width:"width"},function(name,type){jQuery.each({padding:"inner"+name,content:type,"":"outer"+name},function(defaultExtra,funcName){jQuery.fn[funcName]=function(margin,value){var chainable=arguments.length&&(defaultExtra||typeof margin!=="boolean"),extra=defaultExtra||(margin===true||value===true?"margin":"border");return jQuery.access(this,function(elem,type,value){var doc;if(jQuery.isWindow(elem)){return elem.document.documentElement["client"+name];}
+if(elem.nodeType===9){doc=elem.documentElement;return Math.max(elem.body["scroll"+name],doc["scroll"+name],elem.body["offset"+name],doc["offset"+name],doc["client"+name]);}
+return value===undefined?jQuery.css(elem,type,extra):jQuery.style(elem,type,value,extra);},type,chainable?margin:undefined,chainable,null);};});});jQuery.fn.size=function(){return this.length;};jQuery.fn.andSelf=jQuery.fn.addBack;if(typeof module==="object"&&module&&typeof module.exports==="object"){module.exports=jQuery;}else{if(typeof define==="function"&&define.amd){define("jquery",[],function(){return jQuery;});}}
+if(typeof window==="object"&&typeof window.document==="object"){window.jQuery=window.$=jQuery;}})(window);
diff --git a/static/js/manage.js b/static/js/manage.js
new file mode 100644
index 0000000..10898d0
--- /dev/null
+++ b/static/js/manage.js
@@ -0,0 +1,22 @@
+function addtime(e) {
+ e.preventDefault();
+ document.getElementById("seconds").value = this.dataset.secs;
+}
+
+function pvw(e) {
+ prev = document.getElementById("prev_desc");
+ prev.innerHTML = desc.value;
+ prev.style.display = "block";
+ prev.addEventListener("input", function() { desc.value = prev.innerHTML; });
+}
+
+document.addEventListener("DOMContentLoaded", function(e) {
+ var list = document.getElementById("timelist");
+ if (list) {
+ var secs = document.getElementById("timelist").getElementsByTagName("a");
+ for(var i=0;i<secs.length;i++) secs[i].addEventListener("click", addtime);
+ }
+
+ desc = document.getElementById("brd_desc");
+ if (desc) desc.addEventListener("input", pvw);
+}); \ No newline at end of file
diff --git a/static/js/mobile.js b/static/js/mobile.js
new file mode 100644
index 0000000..b4dd8cf
--- /dev/null
+++ b/static/js/mobile.js
@@ -0,0 +1,447 @@
+function sendPost(e) {
+ e.preventDefault();
+ var button = document.getElementById("post");
+ button.disabled = true;
+ var sendpost = new XMLHttpRequest();
+ var postform = document.getElementById("postform");
+ sendpost.open("POST", "/cgi/api/post", true);
+ sendpost.send(new FormData(postform));
+ sendpost.onreadystatechange = function() {
+ if (sendpost.readyState == 4) {
+ button.disabled = false;
+ var response = JSON.parse(sendpost.responseText);
+ if (response.state == "success") { postform.message.value = ""; checkNew(e); }
+ else alert(response.message);
+ }
+ }
+}
+
+function postClick(e) {
+ e.preventDefault();
+ var sel = window.getSelection().toString();
+ if (sel) { sel=sel.replace(/^/gm, ">")+"\n"; sel="\n"+sel; }
+ insert(">>" + parseInt(this.innerHTML, 10) + sel);
+}
+
+function insert(text) {
+ var textarea=document.forms.postform.message;
+ if(textarea) {
+ if(textarea.createTextRange && textarea.caretPos) { // IE
+ var caretPos=textarea.caretPos;
+ caretPos.text=caretPos.text.charAt(caretPos.text.length-1)==" "?text+" ":text;
+ } else if(textarea.setSelectionRange) { // Firefox
+ var start=textarea.selectionStart;
+ var end=textarea.selectionEnd;
+ textarea.value=textarea.value.substr(0,start)+text+textarea.value.substr(end);
+ textarea.setSelectionRange(start+text.length,start+text.length);
+ } else {
+ textarea.value+=text+" ";
+ }
+ textarea.focus();
+ }
+ return false;
+}
+
+function getPassword() {
+ if (weabot.password) return weabot.password;
+ var char="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var pass="";
+ for (var i=0;i<8;i++) {
+ var rnd = Math.floor(Math.random()*char.length);
+ pass += char.substring(rnd, rnd+1);
+ }
+ console.log(weabot.password);
+ weabot.password = pass;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+ return(pass);
+}
+
+function saveInputs(e) {
+ var e = e || window.event;
+ var form = e.target || e.srcElement;
+ if(typeof(form.fielda) !== "undefined") weabot.name = form.fielda.value;
+ if(typeof(form.fielda) !== "undefined") weabot.email = form.fieldb.value;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+}
+
+function setInputs() {
+ with(document.getElementById("postform")) {
+ if(typeof(fielda) !== 'undefined' && !fielda.value && weabot.name) fielda.value = weabot.name;
+ if(typeof(fielda) !== 'undefined' && !fieldb.value && weabot.email) fieldb.value = weabot.email;
+ if(!password.value) password.value = getPassword();
+ addEventListener("submit", saveInputs);
+ }
+}
+
+function showMenu(e) {
+ e.preventDefault();
+ if (document.getElementById("mnu-opened")) closeMenu(e);
+ this.id = "mnu-opened";
+ var brd = postform.board.value;
+ var post = this.parentNode.parentNode;
+ if (document.body.className === "txt") {
+ var id = post.id.substr(1);
+ var num = parseInt(post.getElementsByClassName("num")[0].innerText, 10);
+ } else {
+ var id = post.getElementsByClassName("num")[0].innerText;
+ var num = ((post.className === "first") ? 1 : 0);
+ }
+ var menu = document.createElement("div");
+ menu.id = "mnu-list";
+ menu.style.top = (e.pageY + 5) + "px";
+ menu.style.left = (e.pageX + 5) + "px";
+ document.body.appendChild(menu);
+ menu = document.getElementById("mnu-list");
+ var rep = document.createElement("a");
+ rep.href = "#";
+ rep.innerText = "Denunciar post";
+ rep.addEventListener("click", function(e) {
+ var reason = prompt("Razón de denuncia:");
+ if (reason === "") while(reason === "") reason = prompt("Error: Ingresa una razón.");
+ if (reason) {
+ var rep_req = new XMLHttpRequest();
+ var report = "/cgi/report/" + brd + "/" + id + ((num) ? "/" + num : "") + "?reason=" + reason;
+ rep_req.open("GET", report, true);
+ rep_req.send();
+ rep_req.onreadystatechange = function() {
+ if (rep_req.readyState == 4 && rep_req.status == 200) alert("Denuncia enviada.");
+ }
+ }
+ });
+ menu.appendChild(rep);
+ var del = document.createElement("a");
+ del.href = "#";
+ del.innerText = "Eliminar post";
+ del.addEventListener("click", function(e) {
+ if(confirm("¿Seguro que deseas borrar el mensaje "+((num) ? num : id)+"?")) {
+ var del_req = new XMLHttpRequest();
+ var del_form = "/cgi/api/delete?dir=" + brd + "&id=" + id + "&password=" + postform.password.value;
+ del_req.open("GET", del_form, true);
+ del_req.send();
+ del_req.onreadystatechange = function() {
+ if (del_req.readyState == 4) {
+ var response = JSON.parse(del_req.responseText);
+ if (response.state == "success") {
+ if (num == 1) {
+ alert("Hilo eliminado.");
+ document.location = "/cgi/mobile/" + brd;
+ } else {
+ alert("Mensaje eliminado.");
+ location.reload();
+ }
+ } else if (response.state == "failed") alert(response.message);
+ }
+ }
+ }
+ });
+ menu.appendChild(del);
+ var file = post.getElementsByClassName("thm")[0];
+ if (file) {
+ var dfile = document.createElement("a");
+ dfile.href = "#";
+ dfile.innerText = "Eliminar archivo";
+ dfile.addEventListener("click", function(e) {
+ if(confirm("¿Seguro que deseas borrar el archivo del mensaje "+((num) ? num : id)+"?")) {
+ var fdel_req = new XMLHttpRequest();
+ var fdel_form = "/cgi/api/delete?dir=" + brd + "&id=" + id + "&password=" + postform.password.value + "&imageonly=true";
+ fdel_req.open("GET", fdel_form, true);
+ fdel_req.send();
+ fdel_req.onreadystatechange = function() {
+ if (fdel_req.readyState == 4) {
+ var response = JSON.parse(fdel_req.responseText);
+ if (response.state == "success") {
+ alert("Archivo eliminado.");
+ post.removeChild(file);
+ } else if (response.state == "failed") alert(response.message);
+ }
+ }
+ }
+ });
+ menu.appendChild(dfile);
+ }
+ e.stopPropagation();
+ this.removeEventListener("click", showMenu);
+ document.addEventListener("click", closeMenu);
+}
+
+function closeMenu(e) {
+ var menu = document.getElementById("mnu-list");
+ menu.parentElement.removeChild(menu);
+ document.removeEventListener("click", closeMenu);
+ var btn = document.getElementById("mnu-opened");
+ btn.addEventListener("click", showMenu);
+ btn.removeAttribute("id");
+ e.preventDefault();
+}
+
+function searchSubjects() {
+ var filter = document.getElementById("search").value.toLowerCase();
+ var nodes = document.getElementsByClassName("list")[0].getElementsByTagName("a");
+ for (i = 0; i < nodes.length; i++) {
+ if (nodes[i].innerHTML.toLowerCase().split(/<\/?br[^>]*>\s*/im)[0].includes(filter))
+ nodes[i].removeAttribute("style");
+ else nodes[i].style.display = "none";
+ }
+}
+
+function searchCatalog() {
+ var filter = document.getElementById("catsearch").value.toLowerCase();
+ var nodes = document.getElementsByClassName("cat");
+ for (i = 0; i < nodes.length; i++) {
+ if (nodes[i].innerText.toLowerCase().substring(nodes[i].innerText.indexOf("R)")+2).includes(filter))
+ nodes[i].removeAttribute("style");
+ else nodes[i].style.display = "none";
+ }
+}
+
+var lastTime = 0;
+var refreshInterval;
+var refreshMaxTime = 30;
+var refreshTime;
+var manual = 0;
+var serviceType = 0;
+var thread_length = 0;
+var thread_lastreply = 0;
+var thread_title = "";
+var thread_first_length = 0;
+var http_request = new XMLHttpRequest();
+
+function checkNew(e) {
+ e.preventDefault();
+ manual = 1;
+ loadJSON();
+ if (chk.checked) refreshMaxTime = 25;
+}
+
+function loadJSON() {
+ if (chk.checked) stopCounter("...");
+ if (manual) {
+ document.getElementById("n").style.color = "gray";
+ document.getElementById("n").innerText = "Revisando...";
+ }
+ var data_file;
+ if (serviceType)
+ data_file = "/cgi/api/thread?dir=" + postform.board.value + "&id=" + postform.parent.value + "&offset=" + thread_length + "&time=" + lastTime;
+ else return false;
+ http_request.open("GET", data_file, true);
+ http_request.send();
+}
+
+function updateThread(posts, total_replies, serverTime) {
+ thread_div = document.getElementById("thread");
+ last_elem = document.getElementById("n");
+
+ for (var i = 0; i < posts.length; i++) {
+ post = posts[i];
+ brd = postform.board.value;
+ var div = document.createElement('div');
+ div.className = "pst";
+ div.id = "p" + post.id;
+ if (post.IS_DELETED == 0) {
+ s_name = post.name;
+ if (post.tripcode) s_name += ' ' + post.tripcode;
+ s_time = post.timestamp_formatted.replace(/\(.{1,3}\)/g, " ");
+ if (post.file)
+ s_img = '<a href="/' + brd + '/src/' + post.file + '" target="_blank" class="thm"><img src="/' + brd + '/mobile/' + post.thumb + '" /><br />' + Math.round(post.file_size/1024) + 'KB ' + post.file.substring(post.file.lastIndexOf(".")+1, post.file.length).toUpperCase() + '</a>';
+ else s_img = '';
+ }
+ if (serviceType == 1) {
+ var pad = "0000" + (thread_length + i + 1);
+ pad = pad.substr(pad.length-4);
+ if (post.IS_DELETED == 0)
+ div.innerHTML = '<h3><a href="#" class="num">' + pad + '</a> ' + s_name + '</h3>' + s_img + '<div class="msg">' + post.message + '</div><h4>' + s_time + '<a href="#" class="mnu">|||</a></h4>';
+ else if (post.IS_DELETED == 1)
+ div.innerHTML = '<h3 class="del"><a href="#" class="num">' + pad + '</a> : Eliminado por el usuario.</h3>';
+ else
+ div.innerHTML = '<h3 class="del"><a href="#" class="num">' + pad + '</a> : Eliminado por miembro del staff.</h3>';
+ } else {
+ if (post.IS_DELETED == 0) {
+ div.innerHTML = '<h3>' + s_name + ' ' + s_time + ' <a href="#" class="num" name="' + post.id + '">' + post.id + '</a><a href="#" class="mnu">|||</a></h3>' + s_img + '<div class="msg">' + post.message + '</div>';
+ } else if (post.IS_DELETED == 1) { div.innerHTML = '<h3 class="del"><a name="' + post.id + '"></a>No.' + post.id + ' eliminado por el usuario.</h3>'; }
+ else { div.innerHTML = '<h3 class="del"><a name="' + post.id + '"></a>No.' + post.id + ' eliminado por miembro del staff.</h3>'; }
+ }
+
+ div.getElementsByClassName("mnu")[0].addEventListener("click", showMenu);
+ div.getElementsByClassName("num")[0].addEventListener("click", postClick);
+ thread_div.insertBefore(div, last_elem);
+ document.getElementsByTagName("h1")[0].getElementsByTagName("span")[0].innerText = "(" + (thread_length + i + 1) + ")"
+ }
+
+ if (posts.length > 0) {
+ if (!manual) refreshMaxTime = 10;
+ if (!document.hasFocus())
+ if (posts.length > 1) notif(thread_title, posts.length + ' nuevos mensajes');
+ else notif(thread_title, 'Un nuevo mensaje');
+ } else { if (refreshMaxTime <= 60) refreshMaxTime += 5; }
+
+ thread_length = parseInt(total_replies) + 1;
+ new_unread = thread_length - thread_first_length;
+
+ if (new_unread) document.title = '(' + new_unread + ') ' + thread_title;
+ else document.title = thread_title;
+}
+
+function notif(title, msg) {
+ var n = new Notification(title, { body: msg });
+ setTimeout(n.close.bind(n), 10000);
+}
+
+function counter() {
+ if (refreshTime < 1) loadJSON();
+ else {
+ refreshTime--;
+ document.getElementById("counter").innerHTML = (refreshTime + 1);
+ }
+}
+
+function detectService() {
+ if (document.getElementById("thread")) {
+ thread_title = document.getElementsByTagName("h1")[0].innerHTML.split(" \<span\>")[0] + " - " + document.title;
+ thread_length = parseInt(document.getElementsByTagName("h1")[0].getElementsByTagName("span")[0].innerText.slice(1, -1), 10);
+ thread_first_length = thread_length;
+ if (document.body.className === "txt") {
+ serviceType = 1;
+ replylist = document.getElementsByClassName("pst");
+ thread_lastreply = parseInt(replylist[replylist.length - 1].getElementsByClassName("num")[0].innerText);
+ if (thread_length == thread_lastreply) {
+ serviceType = 1;
+ document.getElementById("n2").setAttribute("style", "border-top:1px solid #c6c7c8;border-left:1px solid #c6c7c8;display:inline-block;text-align:center;width:50%;");
+ return true;
+ } else return false;
+ } else if (document.body.className === "img") {
+ serviceType = 2;
+ document.getElementById("n").innerText = "Ver nuevos posts";
+ document.getElementById("n2").setAttribute("style", "border-top:1px solid #333;border-left:1px solid #333;display:inline-block;text-align:center;width:50%;");
+ replylist = document.getElementsByClassName("first");
+ replylist += document.getElementsByClassName("pst");
+ return true;
+ }
+ } else return false;
+}
+
+function startCounter() {
+ refreshTime = refreshMaxTime;
+ counter();
+ refreshInterval = setInterval(counter, 1000);
+}
+
+function stopCounter(str) {
+ clearInterval(refreshInterval);
+ document.getElementById("counter").innerHTML = str;
+}
+
+function autoRefresh(e) {
+ chk_snd = document.getElementById("autosound");
+ if (document.getElementById("autorefresh").checked) {
+ if (chk_snd) chk_snd.disabled = false;
+ Notification.requestPermission();
+ lastTime = Math.floor(Date.now() / 1000);
+ refreshTime = refreshMaxTime;
+ startCounter();
+ } else {
+ if (chk_snd) document.getElementById("autosound").disabled = true;
+ stopCounter("OFF");
+ }
+}
+
+http_request.onreadystatechange = function() {
+ if (http_request.readyState == 4) {
+ var jsonObj = JSON.parse(http_request.responseText);
+ if (jsonObj.state == "success") {
+ updateThread(jsonObj.posts, jsonObj.total_replies, jsonObj.time);
+ lastTime = jsonObj.time;
+ if (chk.checked) startCounter();
+ }
+ if (manual) {
+ document.getElementById("n").style.color = "inherit";
+ document.getElementById("n").innerText = "Ver nuevos posts";
+ }
+ manual = 0;
+ }
+}
+
+function sortList(type) {
+ for(var i=0;i<srts.length;i++) srts[i].removeAttribute("class");
+ srts[type].className = "sel";
+ var cont = document.getElementById("to_sort");
+ var elem = cont.getElementsByTagName("a");
+ var arr = Array.prototype.slice.call(elem);
+ if (type==0) arr.sort(function (a,b) { return (a.dataset.num-b.dataset.num) });
+ else if (type==1) arr.sort(function (a,b) { return (b.dataset.id-a.dataset.id) });
+ else if (type==2) arr.sort(function (a,b) { return (a.dataset.id-b.dataset.id) });
+ else if (type==3) arr.sort(function (a,b) { return (b.dataset.res-a.dataset.res) });
+ else if (type==4) arr.sort(function (a,b) { return (a.dataset.res-b.dataset.res) });
+ for (var j=0;j<arr.length;j++) cont.appendChild(arr[j]);
+}
+
+document.addEventListener("DOMContentLoaded", function(e) {
+ if (localStorage.hasOwnProperty("weabot")) weabot = JSON.parse(localStorage.getItem("weabot"));
+ else weabot = {"name":null,"email":null,"password":null};
+
+ var ids = document.getElementsByClassName("num");
+ for(var i=0;i<ids.length;i++) ids[i].addEventListener("click", postClick);
+
+ var form = document.getElementById("postform");
+ if (form) {
+ setInputs();
+ if (document.getElementById("post").value == "Responder")
+ form.addEventListener("submit", sendPost);
+ }
+
+ if (document.getElementById("search")) document.getElementById("search").addEventListener("keyup", searchSubjects);
+ if (document.getElementById("catsearch")) document.getElementById("catsearch").addEventListener("keyup", searchCatalog);
+ if (document.getElementById("to_sort")) {
+ srts = document.getElementsByClassName("ord")[0].getElementsByTagName("a");
+ for(var i=0;i<srts.length;i++) srts[i].addEventListener("click", function(e) { e.preventDefault(); sortList(this.dataset.sort); });
+ }
+
+ if (document.getElementById("thread")) {
+ var mnu = document.createElement('a');
+ mnu.href = "#";
+ mnu.className = "mnu";
+ mnu.innerHTML = "|||";
+ if (document.body.className === "txt") var ft = document.getElementsByTagName("h4");
+ else if (document.body.className === "img") var ft = document.getElementsByTagName("h3");
+ for(var i=0;i<ft.length;i++) {
+ if (!ft[i].classList.contains("del")) {
+ var cln = mnu.cloneNode(true);
+ cln.addEventListener("click", showMenu);
+ ft[i].appendChild(cln);
+ }
+ }
+ }
+
+ if (!detectService()) return;
+ document.title = thread_title;
+ document.getElementById("n").style.display = "inline-block";
+ document.getElementById("n").style.width = "50%";
+ document.getElementById("n").addEventListener("click", checkNew);
+ var lbl = document.createElement("label");
+ lbl.id = "auto";
+ lbl.style.display = "block";
+ lbl.style.padding = "6px 0";
+ var btn = document.createElement("input");
+ btn.id = "autorefresh";
+ btn.setAttribute("type", "checkbox");
+ btn.addEventListener("click", autoRefresh);
+ var cnt = document.createElement("span");
+ cnt.id = "counter";
+ cnt.textContent = "OFF";
+ document.getElementById("n2").appendChild(lbl);
+ document.getElementById("auto").appendChild(btn);
+ document.getElementById("auto").appendChild(document.createTextNode(" Auto "));
+ document.getElementById("auto").appendChild(cnt);
+
+ chk = document.getElementById("autorefresh");
+ if (localStorage.getItem("autorefreshmobile")) {
+ chk.checked = true;
+ autoRefresh();
+ }
+});
+
+window.addEventListener("unload", function() {
+ chk = document.getElementById("autorefresh");
+ if (!serviceType) return;
+ if (chk.checked) localStorage.setItem("autorefreshmobile", true);
+ else localStorage.removeItem("autorefreshmobile");
+}); \ No newline at end of file
diff --git a/static/js/paintbbs/PaintBBS-1.1.11.css b/static/js/paintbbs/PaintBBS-1.1.11.css
new file mode 100644
index 0000000..776f63d
--- /dev/null
+++ b/static/js/paintbbs/PaintBBS-1.1.11.css
@@ -0,0 +1,535 @@
+.NEO {
+ margin:0;
+ line-height:18px;
+
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+ touch-callout: none;
+ -webkit-touch-callout: none;
+}
+
+.NEO *:active,
+.NEO *:hover {
+ cursor: default;
+}
+
+.NEO #container{
+ width: 100%;
+ height: 100%;
+ position: relative;
+ border: 1px dotted transparent;
+}
+
+.NEO #center{
+ display: table;
+ position: relative;
+ height: 100%;
+ margin: auto;
+}
+
+.NEO #toolsWrapper{
+ width: 52px;
+ display: table;
+ position:absolute;
+ top: 0;
+ right: -3px;
+}
+
+.NEO #tools{
+ width: 52px;
+ display: table-cell;
+ vertical-align: middle;
+ position: relative;
+}
+
+.NEO #painterContainer{
+ display:table-cell;
+ vertical-align: middle;
+ position: relative;
+}
+
+.NEO #painterWrapper{
+ padding: 0 55px 0 0;
+ position: relative;
+
+ float:right; /* ãªãœã‹Chrome55ã§ãšã‚Œã‚‹ã®ã§å¯¾ç­– */
+}
+
+.NEO #upper {
+ height:30px;
+ padding-right: 20px;
+ text-align:right;
+}
+
+.NEO #lower {
+ height:30px;
+}
+
+.NEO #painter {
+ position: relative;
+ padding: 20px 20px 20px 20px;
+}
+
+.NEO #canvas {
+ position: relative;
+ width: 300px;
+ height: 300px;
+ background-color: white;
+}
+
+.NEO #headerButtons {
+ position: absolute;
+ top: 5px;
+ left: 5px;
+}
+
+.NEO #footerButtons {
+ position: absolute;
+ bottom: 5px;
+ left: 5px;
+}
+
+
+/*
+-----------------------
+ Modal Window
+-----------------------
+*/
+
+.NEO #pageView{
+ background-color: white;
+}
+
+.NEO #windowView{
+ z-index: 9999;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: white;
+}
+
+/*
+-----------------------
+ Controls
+-----------------------
+*/
+
+.NEO #zoomMinus {
+ padding: 0;
+ margin: 0;
+ width:16px;
+ height:16px;
+ margin-left:1px;
+}
+
+.NEO #zoomPlus {
+ padding: 0;
+ margin: 0;
+ width:16px;
+ height:16px;
+}
+
+.NEO #zoomMinusWrapper {
+ position: absolute;
+ right: -20px;
+ bottom: -21px;
+ width: 19px;
+ height: 19px;
+ text-align:center;
+}
+
+.NEO #zoomPlusWrapper {
+ position: absolute;
+ left: -21px;
+ bottom: -21px;
+ width: 19px;
+ height: 19px;
+ text-overflow: hidden;
+ text-align:center;
+}
+
+.NEO #scrollH {
+ position: absolute;
+ left: 1px;
+ bottom: -20px;
+ width: calc(100% - 2px);
+ height: 18px;
+}
+
+.NEO #scrollV {
+ position: absolute;
+ right: -20px;
+ top: 1px;
+ width: 18px;
+ height: calc(100% - 2px);
+}
+
+.NEO #scrollH div {
+ position: absolute;
+ width: 50px;
+ height: 16px;
+}
+
+.NEO #scrollV div {
+ position: absolute;
+ width: 16px;
+ height: 50px;
+}
+
+.NEO #scrollH div:hover {}
+.NEO #scrollV div:hover {}
+
+.NEO #neoWarning {
+ position: absolute;
+ left: 5px;
+ top: 5px;
+ color: red;
+ pointer-events: none;
+ text-align: left;
+ transition: all 2s;
+}
+
+/*
+-----------------------
+ Widgets
+-----------------------
+*/
+
+.NEO .buttonOff {
+ display: inline-block;
+ border: 1px solid white;
+ height: 19px;
+ padding: 3px;
+ height: 16px;
+ font-size: 15px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ margin-right: 4px;
+
+ border-left: 1px solid rgba(0, 0, 0, 0);
+ border-top: 1px solid rgba(0, 0, 0, 0);
+ border-right: 1px solid rgba(0, 0, 0, 0);
+ border-bottom: 1px solid rgba(0, 0, 0, 0);
+
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ margin: 1px;
+
+}
+
+.NEO .buttonOff:hover {}
+
+.NEO .buttonOff:active,
+.NEO .buttonOn {
+ display: inline-block;
+ height: 19px;
+ padding: 3px;
+ height: 16px;
+ font-size: 15px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ margin-right: 4px;
+
+ border-right: 1px solid rgba(0, 0, 0, 0);
+ border-bottom: 1px solid rgba(0, 0, 0, 0);
+
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ margin: 1px;
+}
+
+.NEO .toolTipOff,
+.NEO .toolTipFixed {
+ position:relative;
+ padding: 0;
+ margin: 0;
+ margin-top: 3px;
+ margin-bottom: 3px;
+
+ width: 46px;
+ height: 18px;
+ border-top: 1px solid #ffffff;
+ border-left: 1px solid #ffffff;
+ border-right: 1px solid #9397b2;
+ border-bottom: 1px solid #9397b2;
+}
+
+.NEO .toolTipOn {
+ position:relative;
+ padding: 0;
+ margin: 0;
+ margin-top: 3px;
+ margin-bottom: 3px;
+
+ width: 46px;
+ height: 18px;
+ border-top: 1px solid rgba(0, 0, 0, 0);
+ border-left: 1px solid rgba(0, 0, 0, 0);
+ border-right: 1px solid #ffffff;
+ border-bottom: 1px solid #ffffff;
+}
+
+.NEO .toolTipOff canvas,
+.NEO .toolTipOn canvas,
+.NEO .toolTipFixed canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.NEO .toolTipOff .label,
+.NEO .toolTipOn .label,
+.NEO .toolTipFixed .label {
+ position: absolute;
+ bottom: -4px;
+ left: 1px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: -1px;
+}
+
+.NEO .colorTips {
+ position:relative;
+ width: 48px;
+ height: 144px;
+ padding: 0;
+ margin: 0;
+ margin-top: 4px;
+ margin-left: 0px;
+}
+
+.NEO .colorTipOff {
+ position: absolute;
+ overflow: hidden;
+ width: 22px;
+ height: 18px;
+ margin: -1px 4px 0px 0px;
+ padding: 0;
+}
+
+.NEO .colorTipOff img {
+ left: 0;
+ opacity: 0.5;
+ position: absolute;
+ pointer-events:none;
+}
+
+.NEO .colorTipOn {
+ position: absolute;
+ overflow: hidden;
+ width: 22px;
+ height: 18px;
+ margin: -1px 4px 0px 0px;
+ padding: 0;
+}
+
+.NEO .colorTipOn img {
+ left: -22px;
+ opacity: 0.5;
+ position: absolute;
+ pointer-events:none;
+}
+
+.NEO .colorSlider {
+ width: 48px;
+ height:13px;
+ position: relative;
+ margin-top: 3px;
+}
+
+.NEO .colorSlider .label {
+ pointer-events: none;
+ position: absolute;
+ left: 2px;
+ bottom: -3px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: middle;
+}
+
+.NEO .colorSlider .slider {
+ position: absolute;
+ height: 100%;
+ left: 0px;
+ width: 50%;
+ background-color: #fa9696;
+ box-shadow: -1px 0 0 0px rgba(0, 0, 0, 0.3) inset;
+}
+
+.NEO .colorSlider .hit {
+ position:absolute;
+ width: 60px;
+ height: 13px;
+ left: -6px;
+ background-color: white;
+ opacity: 0.01;
+}
+
+.NEO .sizeSlider {
+ width: 48px;
+ height:33px;
+ position: relative;
+ margin-top: 4px;
+}
+
+.NEO .sizeSlider .label {
+ pointer-events: none;
+ position: absolute;
+ left: 2px;
+ bottom: -3px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: middle;
+}
+
+.NEO .sizeSlider .slider {
+ position: absolute;
+ width: 100%;
+ top: 0px;
+ height: 33%;
+ background-color: #82f238;
+ box-shadow: 0 -1px 0 0px rgba(0, 0, 0, 0.3) inset;
+}
+
+.NEO .sizeSlider .hit {
+ position:absolute;
+ width: 48px;
+ height: 41px;
+ top: -4px;
+ background-color: white;
+ opacity: 0.01;
+}
+
+.NEO .reserveControl {
+ width: 48px;
+ height: 13px;
+ position: relative;
+}
+
+.NEO .reserveControl .reserve {
+ position: absolute;
+ width: 11px;
+ height: 8px;
+ background-color: white;
+}
+
+.NEO .layerControl {
+ width: 48px;
+ height: 20px;
+ position: relative;
+ margin-top: 6px;
+}
+
+.NEO .layerControl .bg {
+ position: absolute;
+ width: 44px;
+ height: 9px;
+ margin: 0;
+
+ top: 0px;
+ left: 2px;
+}
+
+.NEO .layerControl .line1 {
+ position: absolute;
+ width: 50px;
+ height:10px;
+ margin: 0;
+ padding: 0;
+
+ top: 0px;
+ left: -1px;
+ background-image: linear-gradient(to top right, transparent, transparent 47%, red 47%, red 53%, transparent 53%, transparent);
+}
+
+.NEO .layerControl .line0 {
+ position: absolute;
+ width: 50px;
+ height: 10px;
+ margin: 0;
+ padding: 0;
+
+ top: 10px;
+ left: -1px;
+ background-image: linear-gradient(to top right, transparent, transparent 47%, red 47%, red 53%, transparent 53%, transparent);
+}
+
+.NEO .layerControl .label1 {
+ position: absolute;
+ left: 2px;
+ Top: -4px;
+ font-size:11px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: baseline;
+}
+
+.NEO .layerControl .label0 {
+ position: absolute;
+ left: 2px;
+ Top: 6px;
+ font-size:11px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: baseline;
+}
+
+/*
+-----------------------
+ InputText
+-----------------------
+*/
+
+.NEO .inputText {
+ z-index: 100000;
+ position: absolute;
+
+ text-align: left;
+ white-space: nowrap;
+ color: #0000ff;
+
+ font-family: 'Arial';
+ padding: 1px 5px 1px 5px;
+ font-size: 32px;
+
+ line-height: 40px;
+ height: 40px;
+ min-width: 10em;
+
+ bottom: 0px;
+ left: 250px;
+
+ border: 1px solid #aaa;
+ background-color: white;
+}
+
+*[contenteditable] {
+ user-select: auto !important;
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto !important;
+}
+
+#testtext {
+ user-select: auto !important;
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto;
+}
+
+[contenteditable="true"]:focus {
+ outline: 1px solid #b1d6fd;
+ border: 1px solid #b1d6fd;
+}
diff --git a/static/js/paintbbs/PaintBBS-1.1.11.js b/static/js/paintbbs/PaintBBS-1.1.11.js
new file mode 100644
index 0000000..8ca50e7
--- /dev/null
+++ b/static/js/paintbbs/PaintBBS-1.1.11.js
@@ -0,0 +1,5686 @@
+'use strict';
+
+document.addEventListener("DOMContentLoaded", function() {
+ Neo.init();
+
+ if (!navigator.userAgent.match("Electron")) {
+ Neo.start();
+ }
+});
+
+
+var Neo = function() {};
+
+Neo.version = "1.1.11";
+Neo.painter;
+Neo.fullScreen = false;
+Neo.uploaded = false;
+
+Neo.config = {
+ width: 300,
+ height: 300,
+
+ color_bk: "#ccccff",
+ color_bk2: "#bbbbff",
+ color_tool_icon: "#e8dfae",
+ color_icon: "#ccccff",
+ color_iconselect: "#ffaaaa",
+ color_text: "#666699",
+ color_bar: "#6f6fae",
+
+ tool_color_button: "#e8dfae",
+ tool_color_button2: "#f8daaa",
+ tool_color_text: "#773333",
+ tool_color_bar: "#ddddff",
+ tool_color_frame: "#000000",
+
+ colors: [
+ "#000000", "#FFFFFF",
+ "#B47575", "#888888",
+ "#FA9696", "#C096C0",
+ "#FFB6FF", "#8080FF",
+ "#25C7C9", "#E7E58D",
+ "#E7962D", "#99CB7B",
+ "#FCECE2", "#F9DDCF"
+ ]
+};
+
+Neo.reservePen = {};
+Neo.reserveEraser = {};
+
+Neo.SLIDERTYPE_RED = 0;
+Neo.SLIDERTYPE_GREEN = 1;
+Neo.SLIDERTYPE_BLUE = 2;
+Neo.SLIDERTYPE_ALPHA = 3;
+Neo.SLIDERTYPE_SIZE = 4;
+
+document.neo = Neo;
+
+Neo.init = function() {
+ var applets = document.getElementsByTagName('applet');
+ if (applets.length == 0) {
+ applets = document.getElementsByTagName('applet-dummy');
+ }
+
+ for (var i = 0; i < applets.length; i++) {
+ var applet = applets[i];
+ var name = applet.attributes.name.value;
+ if (name == "paintbbs") {
+ Neo.applet = applet;
+ Neo.createContainer(applet);
+ Neo.initConfig(applet);
+ Neo.init2();
+ }
+ }
+};
+
+Neo.init2 = function() {
+ var pageview = document.getElementById("pageView");
+ pageview.style.width = Neo.config.applet_width + "px";
+ pageview.style.height = Neo.config.applet_height + "px";
+
+ Neo.canvas = document.getElementById("canvas");
+ Neo.container = document.getElementById("container");
+ Neo.toolsWrapper = document.getElementById("toolsWrapper");
+
+ Neo.painter = new Neo.Painter();
+ Neo.painter.build(Neo.canvas, Neo.config.width, Neo.config.height);
+
+ Neo.container.oncontextmenu = function() {return false;};
+
+ // 続ãã‹ã‚‰æã
+ if (Neo.config.image_canvas) {
+ Neo.painter.loadImage(Neo.config.image_canvas);
+ }
+
+ // æãã‹ã‘ã®ç”»åƒãŒè¦‹ã¤ã‹ã£ãŸã¨ã
+ if (sessionStorage.getItem('timestamp')) {
+ setTimeout(function () {
+ if (confirm("¿Cargar datos guardados?")) {
+ Neo.painter.loadSession();
+ }
+ }, 1);
+ }
+
+ window.addEventListener("beforeunload", function(e) {
+ if (!Neo.uploaded) {
+ Neo.painter.saveSession();
+ } else {
+ Neo.painter.clearSession();
+ }
+ }, false);
+}
+
+Neo.initConfig = function(applet) {
+ if (applet) {
+ var name = applet.attributes.name.value || "neo";
+ var appletWidth = applet.attributes.width;
+ var appletHeight = applet.attributes.height;
+ if (appletWidth) Neo.config.applet_width = parseInt(appletWidth.value);
+ if (appletHeight) Neo.config.applet_height = parseInt(appletHeight.value);
+
+ var params = applet.getElementsByTagName('param');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i];
+ Neo.config[p.name] = Neo.fixConfig(p.value);
+
+ if (p.name == "image_width") Neo.config.width = parseInt(p.value);
+ if (p.name == "image_height") Neo.config.height = parseInt(p.value);
+ }
+
+ var e = document.getElementById("container");
+ Neo.config.inherit_color = Neo.getInheritColor(e);
+ if (!Neo.config.color_frame) Neo.config.color_frame = Neo.config.color_text;
+ }
+
+ Neo.config.reserves = [
+ { size:1,
+ color:"#000000", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_PEN,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ { size:5,
+ color:"#FFFFFF", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_ERASER,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ { size:10,
+ color:"#FFFFFF", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_ERASER,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ ];
+
+ Neo.reservePen = Neo.clone(Neo.config.reserves[0]);
+ Neo.reserveEraser = Neo.clone(Neo.config.reserves[1]);
+};
+
+Neo.fixConfig = function(value) {
+ // javaã§ã¯"#12345"を色ã¨ã—ã¦è§£é‡ˆã™ã‚‹ãŒjavascriptã§ã¯"#012345"ã«å¤‰æ›ã—ãªã„ã¨ã ã‚
+ if (value.match(/^#[0-9a-fA-F]{5}$/)) {
+ value = "#0" + value.slice(1);
+ }
+ return value;
+};
+
+Neo.initSkin = function() {
+ var sheet = document.styleSheets[0];
+ if (!sheet) {
+ var style = document.createElement("style");
+ document.head.appendChild(style); // must append before you can access sheet property
+ sheet = style.sheet;
+ }
+
+ Neo.styleSheet = sheet;
+
+ var lightBorder = Neo.multColor(Neo.config.color_icon, 1.3);
+ var darkBorder = Neo.multColor(Neo.config.color_icon, 0.7);
+ var lightBar = Neo.multColor(Neo.config.color_bar, 1.3);
+ var darkBar = Neo.multColor(Neo.config.color_bar, 0.7);
+ var bgImage = Neo.backgroundImage();
+
+ Neo.addRule(".NEO #container", "background-image", "url(" + bgImage + ")");
+ Neo.addRule(".NEO .colorSlider .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .sizeSlider .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .label1", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .label0", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .toolTipOn .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .toolTipOff .label", "color", Neo.config.tool_color_text);
+
+ Neo.addRule(".NEO #toolSet", "background-color", Neo.config.color_bk);
+ Neo.addRule(".NEO #tools", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .bg", "border-bottom", "1px solid " + Neo.config.tool_color_text);
+
+ Neo.addRule(".NEO .buttonOn", "color", Neo.config.color_text);
+ Neo.addRule(".NEO .buttonOff", "color", Neo.config.color_text);
+
+ Neo.addRule(".NEO .buttonOff", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "border-top", "1px solid ", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "border-left", "1px solid ", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "box-shadow", "0 0 0 1px " + Neo.config.color_icon + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO .buttonOff:hover", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff:hover", "border-top", "1px solid " + lightBorder);
+ Neo.addRule(".NEO .buttonOff:hover", "border-left", "1px solid " + lightBorder);
+ Neo.addRule(".NEO .buttonOff:hover", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "background-color", darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "border-top", "1px solid " + darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "border-left", "1px solid " + darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO #canvas", "border", "1px solid " + Neo.config.color_frame);
+ Neo.addRule(".NEO #scrollH, .NEO #scrollV", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO #scrollH, .NEO #scrollV", "box-shadow", "0 0 0 1px white" + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "background-color", Neo.config.color_bar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "box-shadow", "0 0 0 1px " + Neo.config.color_icon);
+ Neo.addRule(".NEO #scrollH div:hover, .NEO #scrollV div:hover", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect);
+
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-top", "1px solid " + lightBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-left", "1px solid " + lightBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-right", "1px solid " + darkBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-bottom", "1px solid " + darkBar);
+
+ Neo.addRule(".NEO .toolTipOn", "background-color", Neo.multColor(Neo.config.tool_color_button, 0.7));
+ Neo.addRule(".NEO .toolTipOff", "background-color", Neo.config.tool_color_button);
+ Neo.addRule(".NEO .toolTipFixed", "background-color", Neo.config.tool_color_button2);
+
+ Neo.addRule(".NEO .colorSlider, .NEO .sizeSlider", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .reserveControl", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .reserveControl", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .layerControl", "background-color", Neo.config.tool_color_bar);
+
+ Neo.addRule(".NEO .colorTipOn, .NEO .colorTipOff", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .toolTipOn, .NEO .toolTipOff", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .toolTipFixed", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .colorSlider, .NEO .sizeSlider", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .reserveControl", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .layerControl", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .reserveControl .reserve", "border", "1px solid " + Neo.config.tool_color_frame);
+};
+
+Neo.addRule = function(selector, styleName, value, sheet) {
+ if (!sheet) sheet = Neo.styleSheet;
+ if (sheet.addRule) {
+ sheet.addRule(selector, styleName + ":" + value, sheet.rules.length);
+
+ } else if (sheet.insertRule) {
+ var rule = selector + "{" + styleName + ":" + value + "}";
+ var index = sheet.cssRules.length;
+ sheet.insertRule(rule, index);
+ }
+};
+
+Neo.getInheritColor = function(e) {
+ var result = "#000000";
+ while (e && e.style) {
+ if (e.style.color != "") {
+ result = e.style.color;
+ break;
+ }
+ if (e.attributes["text"]) {
+ result = e.attributes["text"].value;
+ break;
+ }
+ e = e.parentNode;
+ }
+ return result;
+};
+
+Neo.backgroundImage = function() {
+ var c1 = Neo.painter.getColor(Neo.config.color_bk) | 0xff000000;
+ var c2 = Neo.painter.getColor(Neo.config.color_bk2) | 0xff000000;
+ var bgCanvas = document.createElement("canvas");
+ bgCanvas.width = 16;
+ bgCanvas.height = 16;
+ var ctx = bgCanvas.getContext("2d");
+ var imageData = ctx.getImageData(0, 0, 16, 16);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var index = 0;
+ for (var y = 0; y < 16; y++) {
+ for (var x = 0; x < 16; x++) {
+ buf32[index++] = (x == 14 || y == 14) ? c2 : c1;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+ return bgCanvas.toDataURL('image/png');
+};
+
+Neo.multColor = function(c, scale) {
+ var r = Math.round(parseInt(c.substr(1, 2), 16) * scale);
+ var g = Math.round(parseInt(c.substr(3, 2), 16) * scale);
+ var b = Math.round(parseInt(c.substr(5, 2), 16) * scale);
+ r = ("0" + Math.min(Math.max(r, 0), 255).toString(16)).substr(-2);
+ g = ("0" + Math.min(Math.max(g, 0), 255).toString(16)).substr(-2);
+ b = ("0" + Math.min(Math.max(b, 0), 255).toString(16)).substr(-2);
+ return '#' + r + g + b;
+};
+
+Neo.initComponents = function() {
+ document.getElementById("copyright").innerHTML += "v" + Neo.version;
+
+ //アプレットã®borderã®å‹•ä½œã‚’エミュレート
+ if (navigator.userAgent.search("FireFox") > -1) {
+ var container = document.getElementById("container");
+ container.addEventListener("mousedown", function(e) {
+ container.style.borderColor = Neo.config.inherit_color;
+ e.stopPropagation();
+ }, false);
+ document.addEventListener("mousedown", function(e) {
+ container.style.borderColor = 'transparent';
+ }, false);
+ }
+
+ // 投稿ã«å¤±æ•—ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã¨ãã¯è­¦å‘Šã‚’表示ã™ã‚‹
+ Neo.showWarning();
+
+ if (Neo.styleSheet) {
+ Neo.addRule("*", "user-select", "none");
+ Neo.addRule("*", "-webkit-user-select", "none");
+ Neo.addRule("*", "-ms-user-select", "none");
+ }
+}
+
+Neo.initButtons = function() {
+ new Neo.Button().init("undo").onmouseup = function() {
+ new Neo.UndoCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("redo").onmouseup = function () {
+ new Neo.RedoCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("window").onmouseup = function() {
+ new Neo.WindowCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("submit").onmouseup = function() {
+ new Neo.SubmitCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("copyright").onmouseup = function() {
+ new Neo.CopyrightCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("zoomPlus").onmouseup = function() {
+ new Neo.ZoomPlusCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("zoomMinus").onmouseup = function() {
+ new Neo.ZoomMinusCommand(Neo.painter).execute();
+ };
+
+ Neo.fillButton = new Neo.Button().init("fill", {type:'fill'});
+
+ // toolTip
+ Neo.penTip = new Neo.PenTip().init("pen");
+ Neo.pen2Tip = new Neo.Pen2Tip().init("pen2");
+ Neo.effectTip = new Neo.EffectTip().init("effect");
+ Neo.effect2Tip = new Neo.Effect2Tip().init("effect2");
+ Neo.eraserTip = new Neo.EraserTip().init("eraser");
+ Neo.drawTip = new Neo.DrawTip().init("draw");
+ Neo.maskTip = new Neo.MaskTip().init("mask");
+
+ Neo.toolButtons = [Neo.fillButton,
+ Neo.penTip,
+ Neo.pen2Tip,
+ Neo.effectTip,
+ Neo.effect2Tip,
+ Neo.drawTip,
+ Neo.eraserTip];
+
+ // colorTip
+ for (var i = 1; i <= 14; i++) {
+ new Neo.ColorTip().init("color" + i, {index:i});
+ };
+
+ // colorSlider
+ Neo.sliders[Neo.SLIDERTYPE_RED] = new Neo.ColorSlider().init(
+ "sliderRed", {type:Neo.SLIDERTYPE_RED});
+ Neo.sliders[Neo.SLIDERTYPE_GREEN] = new Neo.ColorSlider().init(
+ "sliderGreen", {type:Neo.SLIDERTYPE_GREEN});
+ Neo.sliders[Neo.SLIDERTYPE_BLUE] = new Neo.ColorSlider().init(
+ "sliderBlue", {type:Neo.SLIDERTYPE_BLUE});
+ Neo.sliders[Neo.SLIDERTYPE_ALPHA] = new Neo.ColorSlider().init(
+ "sliderAlpha", {type:Neo.SLIDERTYPE_ALPHA});
+
+ // sizeSlider
+ Neo.sliders[Neo.SLIDERTYPE_SIZE] = new Neo.SizeSlider().init(
+ "sliderSize", {type:Neo.SLIDERTYPE_SIZE});
+
+ // reserveControl
+ for (var i = 1; i <= 3; i++) {
+ new Neo.ReserveControl().init("reserve" + i, {index:i});
+ };
+
+ new Neo.LayerControl().init("layerControl");
+ new Neo.ScrollBarButton().init("scrollH");
+ new Neo.ScrollBarButton().init("scrollV");
+};
+
+Neo.start = function(isApp) {
+ if (!Neo.painter) return;
+
+ Neo.initSkin();
+ Neo.initComponents();
+ Neo.initButtons();
+
+ Neo.isApp = isApp;
+ if (Neo.applet) {
+ var name = Neo.applet.attributes.name.value || "paintbbs";
+ Neo.applet.outerHTML = "";
+ document[name] = Neo;
+
+ Neo.resizeCanvas();
+ Neo.container.style.visibility = "visible";
+
+ if (Neo.isApp) {
+ var ipc = require('electron').ipcRenderer;
+ ipc.sendToHost('neo-status', 'ok');
+ }
+ }
+};
+
+Neo.showWarning = function() {
+ var futaba = location.hostname.match(/2chan.net/i);
+ var samplebbs = location.hostname.match(/neo.websozai.jp/i);
+
+ var chrome = navigator.userAgent.match(/Chrome\/(\d+)/i);
+ if (chrome && chrome.length > 1) chrome = chrome[1];
+
+ var edge = navigator.userAgent.match(/Edge\/(\d+)/i);
+ if (edge && edge.length > 1) edge = edge[1];
+
+ var ms = false;
+ if (/MSIE 10/i.test(navigator.userAgent)) {
+ ms = true; // This is internet explorer 10
+ }
+ if (/MSIE 9/i.test(navigator.userAgent) ||
+ /rv:11.0/i.test(navigator.userAgent)) {
+ ms = true; // This is internet explorer 9 or 11
+ }
+
+ var str = "";
+ if (futaba || samplebbs) {
+ if (ms || (edge && edge < 15)) {
+ str = "ã“ã®ãƒ–ラウザã§ã¯<br>投稿ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™<br>";
+ }
+ }
+
+ // ã‚‚ã—<PARAM NAME="neo_warning" VALUE="...">ãŒã‚ã‚Œã°è¡¨ç¤ºã™ã‚‹
+ if (Neo.config.neo_warning) {
+ str += Neo.config.neo_warning;
+ }
+
+ var warning = document.getElementById("neoWarning")
+ warning.innerHTML = str;
+ setTimeout(function() { warning.style.opacity = "0"; }, 15000);
+};
+
+/*
+-----------------------------------------------------------------------
+UIã®æ›´æ–°
+-----------------------------------------------------------------------
+*/
+
+Neo.updateUI = function() {
+ var current = Neo.painter.tool.getToolButton();
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ if (current) {
+ if (current == toolTip) {
+ toolTip.setSelected(true);
+ toolTip.update();
+ } else {
+ toolTip.setSelected(false);
+ }
+ }
+ }
+ if (Neo.drawTip) {
+ Neo.drawTip.update();
+ }
+
+ Neo.updateUIColor(true, false);
+}
+
+Neo.updateUIColor = function(updateSlider, updateColorTip) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.update();
+ }
+
+ if (updateSlider) {
+ for (var i = 0; i < Neo.sliders.length; i++) {
+ var slider = Neo.sliders[i];
+ slider.update();
+ }
+ }
+
+ // パレットを変更ã™ã‚‹ã¨ã
+ if (updateColorTip) {
+ var colorTip = Neo.ColorTip.getCurrent();
+ if (colorTip) {
+ colorTip.setColor(Neo.painter.foregroundColor);
+ }
+ }
+};
+
+/*
+-----------------------------------------------------------------------
+リサイズ対応
+-----------------------------------------------------------------------
+*/
+
+Neo.updateWindow = function() {
+ if (Neo.fullScreen) {
+ document.getElementById("windowView").style.display = "block";
+ document.getElementById("windowView").appendChild(Neo.container);
+
+ } else {
+ document.getElementById("windowView").style.display = "none";
+ document.getElementById("pageView").appendChild(Neo.container);
+ }
+ Neo.resizeCanvas();
+};
+
+Neo.resizeCanvas = function() {
+ var appletWidth = Neo.container.clientWidth;
+ var appletHeight = Neo.container.clientHeight;
+
+ var canvasWidth = Neo.painter.canvasWidth;
+ var canvasHeight = Neo.painter.canvasHeight;
+
+ var width0 = canvasWidth * Neo.painter.zoom;
+ var height0 = canvasHeight * Neo.painter.zoom;
+
+ var width = (width0 < appletWidth - 100) ? width0 : appletWidth - 100;
+ var height = (height0 < appletHeight - 120) ? height0 : appletHeight - 120;
+
+ //width, heightã¯å¶æ•°ã§ãªã„ã¨èª¤å·®ãŒå‡ºã‚‹ãŸã‚
+ width = Math.floor(width / 2) * 2;
+ height = Math.floor(height / 2) * 2;
+
+ Neo.painter.destWidth = width;
+ Neo.painter.destHeight = height;
+
+ Neo.painter.destCanvas.width = width;
+ Neo.painter.destCanvas.height = height;
+ Neo.painter.destCanvasCtx = Neo.painter.destCanvas.getContext("2d");
+ Neo.painter.destCanvasCtx.imageSmoothingEnabled = false;
+ Neo.painter.destCanvasCtx.mozImageSmoothingEnabled = false;
+
+ Neo.canvas.style.width = width + "px";
+ Neo.canvas.style.height = height + "px";
+
+ var top = (Neo.container.clientHeight - toolsWrapper.clientHeight) / 2;
+ Neo.toolsWrapper.style.top = ((top > 0) ? top : 0) + "px";
+
+ if (top < 0) {
+ var s = Neo.container.clientHeight / toolsWrapper.clientHeight;
+ Neo.toolsWrapper.style.transform =
+ "translate(0, " + top + "px) scale(1," + s + ")";
+ } else {
+ Neo.toolsWrapper.style.transform = "";
+ }
+
+ Neo.painter.setZoom(Neo.painter.zoom);
+ Neo.painter.updateDestCanvas(0, 0, canvasWidth, canvasHeight);
+};
+
+/*
+-----------------------------------------------------------------------
+投稿
+-----------------------------------------------------------------------
+*/
+
+Neo.clone = function(src) {
+ var dst = {};
+ for (var k in src) {
+ dst[k] = src[k];
+ }
+ return dst;
+};
+
+Neo.getSizeString = function(len) {
+ var result = String(len);
+ while (result.length < 8) {
+ result = "0" + result;
+ }
+ return result;
+};
+
+Neo.openURL = function(url) {
+ if (Neo.isApp) {
+ require('electron').shell.openExternal(url);
+
+ } else {
+ location.href = url;
+ }
+};
+
+Neo.submit = function(board, blob, thumbnail, thumbnail2) {
+ var url = Neo.config.url_save;
+ console.log("submit url=" + url);
+
+ var headerString = Neo.config.send_header || "";
+ var imageType = Neo.config.send_header_image_type;
+ if (imageType && imageType == "true") {
+ headerString = "image_type=png&" + headerString
+ console.log("header=" + headerString);
+ }
+
+ var header = new Blob([headerString]);
+ var headerLength = this.getSizeString(header.size);
+ var imgLength = this.getSizeString(blob.size);
+
+ var array = ['P', // PaintBBS
+ headerLength,
+ header,
+ imgLength,
+ '\r\n',
+ blob];
+
+ if (thumbnail) {
+ var thumbnailLength = this.getSizeString(thumbnail.size);
+ array.push(thumbnailLength, thumbnail);
+ }
+ if (thumbnail2) {
+ var thumbnail2Length = this.getSizeString(thumbnail2.size);
+ array.push(thumbnail2Length, thumbnail2);
+ }
+
+ var body = new Blob(array, {type: 'application/octet-binary'}); //ã“ã‚ŒãŒå¿…è¦ï¼ï¼
+
+ var request = new XMLHttpRequest();
+ request.open("POST", url, true);
+
+ request.onload = function(e) {
+ console.log(request.response);
+ Neo.uploaded = true;
+
+ var url = Neo.config.url_exit;
+ /*if (url[0] == '/') {
+ url = url.replace(/^.*\//, ''); //よãã‚ã‹ã‚“ãªã„ã‘ã©ã¨ã‚Šã‚ãˆãš
+ }*/
+
+ // ãµãŸã°ã®paintpost.phpã¯ã€ç”»åƒæŠ•ç¨¿ã«æˆåŠŸã™ã‚‹ã¨responseã«
+ // "./futaba.php?mode=paintcom&amp;painttmp=.png"
+ // ã¨ã„ã†æ–‡å­—列を返ã—ã¾ã™ã€‚
+ //
+ // NEOã§ã¯ã€responseã«æ–‡å­—列"painttmp="ãŒå«ã¾ã‚Œã‚‹å ´åˆã¯
+ // <PARAM>ã§æŒ‡å®šã•ã‚ŒãŸurl_exitを無視ã—ã¦ã€ã“ã®URLã«ã‚¸ãƒ£ãƒ³ãƒ—ã—ã¾ã™ã€‚
+ var responseURL = request.response.replace(/&amp;/g, '&');
+ if (responseURL.match(/painttmp=/)) {
+ url = responseURL;
+ }
+ //var exitURL = board + url;
+ var exitURL = url;
+
+ // ã—ãƒã¡ã‚ƒã‚“ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’よã見ãŸã‚‰
+ // response㌠"URL:〜" ã®å½¢ã ã£ãŸå ´åˆã¯ãã“ã¸é£›ã°ã™ã£ã¦æ›¸ã„ã¦ã‚ã‚Šã¾ã—ãŸã€‚
+ // ã“ã£ã¡ã‚’使ã†ã¹ãã§ã—ãŸâ€¦â€¦
+ if (responseURL.match(/^URL:/)) {
+ exitURL = responseURL.replace(/^URL:/, '');
+ }
+
+ location.href = exitURL;
+ };
+ request.onerror = function(e) {
+ console.log("error");
+ };
+ request.onabort = function(e) {
+ console.log("abort");
+ };
+ request.ontimeout = function(e) {
+ console.log("timeout");
+ };
+
+ if (0) { // データã®ãƒ‡ãƒãƒƒã‚°ã®ãŸã‚é€ä¿¡ã™ã‚‹ãƒ‡ãƒ¼ã‚¿ã‚’uint8arrayã«ã‚³ãƒ”ーã—ã¦ãŠã
+ var fr = new FileReader();
+ fr.onload = function () {
+ var result = fr.result;
+ }
+ fr.readAsArrayBuffer(body);
+ }
+
+ request.send(body);
+};
+
+/*
+-----------------------------------------------------------------------
+LiveConnect
+-----------------------------------------------------------------------
+*/
+
+Neo.getColors = function() {
+ console.log("getColors");
+ return Neo.config.colors.join('\n');
+};
+
+Neo.setColors = function(colors) {
+ console.log("setColors");
+ var array = colors.split('\n');
+ for (var i = 0; i < 14; i++) {
+ var color = array[i];
+ Neo.config.colors[i] = color;
+ Neo.colorTips[i].setColor(color);
+ }
+};
+
+/*
+-----------------------------------------------------------------------
+DOMツリーã®ä½œæˆ
+-----------------------------------------------------------------------
+*/
+
+Neo.createContainer = function(applet) {
+ var neo = document.createElement("div");
+ neo.className = "NEO";
+ neo.id = "NEO";
+ neo.innerHTML = (function() {/*
+
+<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
+
+<div id="pageView" style="width:450px; height:470px; margin:auto;">
+ <div id="container" style="visibility:hidden;">
+ <div id="center">
+ <div id="painterContainer">
+ <div id="painterWrapper">
+ <div id="upper">
+ <div id="redo">Rehacer</div>
+ <div id="undo">Deshacer</div>
+ <div id="fill">Llenar</div>
+ </div>
+ <div id="painter">
+ <div id="canvas">
+ <div id="scrollH"></div>
+ <div id="scrollV"></div>
+ <div id="zoomPlusWrapper">
+ <div id="zoomPlus">+</div>
+ </div>
+ <div id="zoomMinusWrapper">
+ <div id="zoomMinus">-</div>
+ </div>
+ <div id="neoWarning"></div>
+ </div>
+ </div>
+ <div id="lower">
+ </div>
+ </div>
+ <div id="toolsWrapper">
+ <div id="tools">
+ <div id="toolSet">
+ <div id="pen"></div>
+ <div id="pen2"></div>
+ <div id="effect"></div>
+ <div id="effect2"></div>
+ <div id="eraser"></div>
+ <div id="draw"></div>
+ <div id="mask"></div>
+
+ <div class="colorTips">
+ <div id="color2"></div><div id="color1"></div><br>
+ <div id="color4"></div><div id="color3"></div><br>
+ <div id="color6"></div><div id="color5"></div><br>
+ <div id="color8"></div><div id="color7"></div><br>
+ <div id="color10"></div><div id="color9"></div><br>
+ <div id="color12"></div><div id="color11"></div><br>
+ <div id="color14"></div><div id="color13"></div>
+ </div>
+
+ <div id="sliderRed"></div>
+ <div id="sliderGreen"></div>
+ <div id="sliderBlue"></div>
+ <div id="sliderAlpha"></div>
+ <div id="sliderSize"></div>
+
+ <div class="reserveControl" style="margin-top:4px;">
+ <div id="reserve1"></div>
+ <div id="reserve2"></div>
+ <div id="reserve3"></div>
+ </div>
+ <div id="layerControl" style="margin-top:6px;"></div>
+
+ <!--<div id="toolPad" style="height:20px;"></div>-->
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="headerButtons">
+ <div id="window">Ventana</div>
+ </div>
+ <div id="footerButtons">
+ <div id="submit">Enviar</div>
+ <div id="copyright">(C)ã—ã„ã¡ã‚ƒã‚“ PaintBBS NEO</div>
+ </div>
+ </div>
+</div>
+
+<div id="windowView" style="display: none;">
+
+</div>
+
+
+*/}).toString().match(/\/\*([^]*)\*\//)[1];
+
+ var parent = applet.parentNode;
+ parent.appendChild(neo);
+ parent.insertBefore(neo, applet);
+
+// applet.style.display = "none";
+
+ // NEOを組ã¿è¾¼ã‚“ã URLをアプリ版ã§é–‹ãã¨DOMツリーãŒ2é‡ã«ã§ãã¦æ ¼å¥½æ‚ªã„ã®ã§æ¶ˆã—ã¦ãŠã
+ setTimeout(function() {
+ var tmp = document.getElementsByClassName("NEO");
+ if (tmp.length > 1) {
+ for (var i = 1; i < tmp.length; i++) {
+ tmp[i].style.display = "none";
+ }
+ }
+ }, 0);
+};
+
+
+'use strict';
+
+Neo.Painter = function() {
+ this._undoMgr = new Neo.UndoManager(50);
+};
+
+Neo.Painter.prototype.container;
+Neo.Painter.prototype._undoMgr;
+Neo.Painter.prototype.tool;
+Neo.Painter.prototype.inputText;
+
+//Canvas Info
+Neo.Painter.prototype.canvasWidth;
+Neo.Painter.prototype.canvasHeight;
+Neo.Painter.prototype.canvas = [];
+Neo.Painter.prototype.canvasCtx = [];
+Neo.Painter.prototype.visible = [];
+Neo.Painter.prototype.current = 0;
+
+//Temp Canvas Info
+Neo.Painter.prototype.tempCanvas;
+Neo.Painter.prototype.tempCanvasCtx;
+Neo.Painter.prototype.tempX = 0;
+Neo.Painter.prototype.tempY = 0;
+
+//Destination Canvas for display
+Neo.Painter.prototype.destCanvas;
+Neo.Painter.prototype.destCanvasCtx;
+
+
+Neo.Painter.prototype.backgroundColor = "#ffffff";
+Neo.Painter.prototype.foregroundColor = "#000000";
+
+Neo.Painter.prototype.lineWidth = 1;
+Neo.Painter.prototype.alpha = 1;
+Neo.Painter.prototype.zoom = 1;
+Neo.Painter.prototype.zoomX = 0;
+Neo.Painter.prototype.zoomY = 0;
+
+Neo.Painter.prototype.isMouseDown;
+Neo.Painter.prototype.isMouseDownRight;
+Neo.Painter.prototype.prevMouseX;
+Neo.Painter.prototype.prevMouseY;
+Neo.Painter.prototype.mouseX;
+Neo.Painter.prototype.mouseY;
+
+Neo.Painter.prototype.slowX = 0;
+Neo.Painter.prototype.slowY = 0;
+Neo.Painter.prototype.stab = null;
+
+Neo.Painter.prototype.isShiftDown = false;
+Neo.Painter.prototype.isCtrlDown = false;
+Neo.Painter.prototype.isAltDown = false;
+
+//Neo.Painter.prototype.onUpdateCanvas;
+Neo.Painter.prototype._roundData = [];
+Neo.Painter.prototype._toneData = [];
+Neo.Painter.prototype.toolStack = [];
+
+Neo.Painter.prototype.maskType = 0;
+Neo.Painter.prototype.drawType = 0;
+Neo.Painter.prototype.maskColor = "#000000";
+Neo.Painter.prototype._currentColor = [];
+Neo.Painter.prototype._currentMask = [];
+
+Neo.Painter.prototype.aerr;
+
+Neo.Painter.LINETYPE_NONE = 0;
+Neo.Painter.LINETYPE_PEN = 1;
+Neo.Painter.LINETYPE_ERASER = 2;
+Neo.Painter.LINETYPE_BRUSH = 3;
+Neo.Painter.LINETYPE_TONE = 4;
+Neo.Painter.LINETYPE_DODGE = 5;
+Neo.Painter.LINETYPE_BURN = 6;
+
+Neo.Painter.MASKTYPE_NONE = 0;
+Neo.Painter.MASKTYPE_NORMAL = 1;
+Neo.Painter.MASKTYPE_REVERSE = 2;
+Neo.Painter.MASKTYPE_ADD = 3;
+Neo.Painter.MASKTYPE_SUB = 4;
+
+Neo.Painter.DRAWTYPE_FREEHAND = 0;
+Neo.Painter.DRAWTYPE_LINE = 1;
+Neo.Painter.DRAWTYPE_BEZIER = 2;
+
+Neo.Painter.ALPHATYPE_NONE = 0;
+Neo.Painter.ALPHATYPE_PEN = 1;
+Neo.Painter.ALPHATYPE_FILL = 2;
+Neo.Painter.ALPHATYPE_BRUSH = 3;
+
+Neo.Painter.TOOLTYPE_NONE = 0;
+Neo.Painter.TOOLTYPE_PEN = 1;
+Neo.Painter.TOOLTYPE_ERASER = 2;
+Neo.Painter.TOOLTYPE_HAND = 3;
+Neo.Painter.TOOLTYPE_SLIDER = 4;
+Neo.Painter.TOOLTYPE_FILL = 5;
+Neo.Painter.TOOLTYPE_MASK = 6;
+Neo.Painter.TOOLTYPE_ERASEALL = 7;
+Neo.Painter.TOOLTYPE_ERASERECT = 8;
+Neo.Painter.TOOLTYPE_COPY = 9;
+Neo.Painter.TOOLTYPE_PASTE = 10;
+Neo.Painter.TOOLTYPE_MERGE = 11;
+Neo.Painter.TOOLTYPE_FLIP_H = 12;
+Neo.Painter.TOOLTYPE_FLIP_V = 13;
+
+Neo.Painter.TOOLTYPE_BRUSH = 14;
+Neo.Painter.TOOLTYPE_TEXT = 15;
+Neo.Painter.TOOLTYPE_TONE = 16;
+Neo.Painter.TOOLTYPE_BLUR = 17;
+Neo.Painter.TOOLTYPE_DODGE = 18;
+Neo.Painter.TOOLTYPE_BURN = 19;
+Neo.Painter.TOOLTYPE_RECT = 20;
+Neo.Painter.TOOLTYPE_RECTFILL = 21;
+Neo.Painter.TOOLTYPE_ELLIPSE = 22;
+Neo.Painter.TOOLTYPE_ELLIPSEFILL = 23;
+Neo.Painter.TOOLTYPE_BLURRECT = 24;
+Neo.Painter.TOOLTYPE_TURN = 25;
+
+Neo.Painter.prototype.build = function(div, width, height)
+{
+ this.container = div;
+ this._initCanvas(div, width, height);
+ this._initRoundData();
+ this._initToneData();
+ this._initInputText();
+
+ this.setTool(new Neo.PenTool());
+
+};
+
+Neo.Painter.prototype.setTool = function(tool) {
+ if (this.tool && this.tool.saveStates) this.tool.saveStates();
+
+ if (this.tool && this.tool.kill) {
+ this.tool.kill();
+ }
+ this.tool = tool;
+ tool.init();
+ if (this.tool && this.tool.loadStates) this.tool.loadStates();
+};
+
+Neo.Painter.prototype.pushTool = function(tool) {
+ this.toolStack.push(this.tool);
+ this.tool = tool;
+ tool.init();
+};
+
+Neo.Painter.prototype.popTool = function() {
+ var tool = this.tool;
+ if (tool && tool.kill) {
+ tool.kill();
+ }
+ this.tool = this.toolStack.pop();
+};
+
+Neo.Painter.prototype.getCurrentTool = function() {
+ if (this.tool) {
+ var tool = this.tool;
+ if (tool && tool.type == Neo.Painter.TOOLTYPE_SLIDER) {
+ var stack = this.toolStack;
+ if (stack.length > 0) {
+ tool = stack[stack.length - 1];
+ }
+ }
+ return tool;
+ }
+ return null;
+};
+
+Neo.Painter.prototype.setToolByType = function(toolType) {
+ switch (parseInt(toolType)) {
+ case Neo.Painter.TOOLTYPE_PEN: this.setTool(new Neo.PenTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASER: this.setTool(new Neo.EraserTool()); break;
+ case Neo.Painter.TOOLTYPE_HAND: this.setTool(new Neo.HandTool()); break;
+ case Neo.Painter.TOOLTYPE_FILL: this.setTool(new Neo.FillTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASEALL: this.setTool(new Neo.EraseAllTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASERECT: this.setTool(new Neo.EraseRectTool()); break;
+
+ case Neo.Painter.TOOLTYPE_COPY: this.setTool(new Neo.CopyTool()); break;
+ case Neo.Painter.TOOLTYPE_PASTE: this.setTool(new Neo.PasteTool()); break;
+ case Neo.Painter.TOOLTYPE_MERGE: this.setTool(new Neo.MergeTool()); break;
+ case Neo.Painter.TOOLTYPE_FLIP_H: this.setTool(new Neo.FlipHTool()); break;
+ case Neo.Painter.TOOLTYPE_FLIP_V: this.setTool(new Neo.FlipVTool()); break;
+
+ case Neo.Painter.TOOLTYPE_BRUSH: this.setTool(new Neo.BrushTool()); break;
+ case Neo.Painter.TOOLTYPE_TEXT: this.setTool(new Neo.TextTool()); break;
+ case Neo.Painter.TOOLTYPE_TONE: this.setTool(new Neo.ToneTool()); break;
+ case Neo.Painter.TOOLTYPE_BLUR: this.setTool(new Neo.BlurTool()); break;
+ case Neo.Painter.TOOLTYPE_DODGE: this.setTool(new Neo.DodgeTool()); break;
+ case Neo.Painter.TOOLTYPE_BURN: this.setTool(new Neo.BurnTool()); break;
+
+ case Neo.Painter.TOOLTYPE_RECT: this.setTool(new Neo.RectTool()); break;
+ case Neo.Painter.TOOLTYPE_RECTFILL: this.setTool(new Neo.RectFillTool()); break;
+ case Neo.Painter.TOOLTYPE_ELLIPSE: this.setTool(new Neo.EllipseTool()); break;
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:this.setTool(new Neo.EllipseFillTool()); break;
+ case Neo.Painter.TOOLTYPE_BLURRECT: this.setTool(new Neo.BlurRectTool()); break;
+ case Neo.Painter.TOOLTYPE_TURN: this.setTool(new Neo.TurnTool()); break;
+
+ default:
+ console.log("unknown toolType " + toolType);
+ break;
+ }
+};
+
+Neo.Painter.prototype._initCanvas = function(div, width, height) {
+ width = parseInt(width);
+ height = parseInt(height);
+ var destWidth = parseInt(div.clientWidth);
+ var destHeight = parseInt(div.clientHeight);
+ this.destWidth = width;
+ this.destHeight = height;
+
+ this.canvasWidth = width;
+ this.canvasHeight = height;
+ this.zoomX = width * 0.5;
+ this.zoomY = height * 0.5;
+
+ for (var i = 0; i < 2; i++) {
+ this.canvas[i] = document.createElement("canvas");
+ this.canvas[i].width = width;
+ this.canvas[i].height = height;
+ this.canvasCtx[i] = this.canvas[i].getContext("2d");
+
+ this.canvas[i].style.imageRendering = "pixelated";
+ this.canvasCtx[i].imageSmoothingEnabled = false;
+ this.canvasCtx[i].mozImageSmoothingEnabled = false;
+ this.visible[i] = true;
+ }
+
+ this.tempCanvas = document.createElement("canvas");
+ this.tempCanvas.width = width;
+ this.tempCanvas.height = height;
+ this.tempCanvasCtx = this.tempCanvas.getContext("2d");
+ this.tempCanvas.style.position = "absolute";
+ this.tempCanvas.enabled = false;
+
+ var array = this.container.getElementsByTagName("canvas");
+ if (array.length > 0) {
+ this.destCanvas = array[0];
+ } else {
+ this.destCanvas = document.createElement("canvas");
+ this.container.appendChild(this.destCanvas);
+ }
+
+ this.destCanvasCtx = this.destCanvas.getContext("2d");
+ this.destCanvas.width = destWidth;
+ this.destCanvas.height = destHeight;
+
+ this.destCanvas.style.imageRendering = "pixelated";
+ this.destCanvasCtx.imageSmoothingEnabled = false;
+ this.destCanvasCtx.mozImageSmoothingEnabled = false;
+
+ var ref = this;
+
+ var container = document.getElementById("container");
+
+ if (window.PointerEvent) {
+ container.addEventListener("pointerdown", function(e) {
+ ref._mouseDownHandler(e); });
+ container.addEventListener("pointerup", function(e) {
+ ref._mouseUpHandler(e); });
+ container.addEventListener("pointermove", function(e) {
+ ref._mouseMoveHandler(e); });
+ container.addEventListener("pointerover", function(e) {
+ ref._rollOverHandler(e); });
+ container.addEventListener("pointerout", function(e) {
+ ref._rollOutHandler(e); });
+
+ } else {
+ container.onmousedown = function(e) {ref._mouseDownHandler(e)};
+ container.onmousemove = function(e) {ref._mouseMoveHandler(e)};
+ container.onmouseup = function(e) {ref._mouseUpHandler(e)};
+ container.onmouseover = function(e) {ref._rollOverHandler(e)};
+ container.onmouseout = function(e) {ref._rollOutHandler(e)};
+
+ container.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ }, true);
+ container.addEventListener("touchmove", function(e) {
+ ref._mouseMoveHandler(e);
+ }, true);
+ container.addEventListener("touchend", function(e) {
+ ref._mouseUpHandler(e);
+ }, true);
+ }
+
+ document.onkeydown = function(e) {ref._keyDownHandler(e)};
+ document.onkeyup = function(e) {ref._keyUpHandler(e)};
+
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype._initRoundData = function() {
+ for (var r = 1; r <= 30; r++) {
+ this._roundData[r] = new Uint8Array(r * r);
+ var mask = this._roundData[r];
+ var d = Math.floor(r / 2.0);
+ var index = 0;
+ for (var x = 0; x < r; x++) {
+ for (var y = 0; y < r; y++) {
+ var xx = x + 0.5 - r/2.0;
+ var yy = y + 0.5 - r/2.0;
+ mask[index++] = (xx*xx + yy*yy <= r*r/4) ? 1 : 0;
+ }
+ }
+ }
+ this._roundData[3][0] = 0;
+ this._roundData[3][2] = 0;
+ this._roundData[3][6] = 0;
+ this._roundData[3][8] = 0;
+
+ this._roundData[5][1] = 0;
+ this._roundData[5][3] = 0;
+ this._roundData[5][5] = 0;
+ this._roundData[5][9] = 0;
+ this._roundData[5][15] = 0;
+ this._roundData[5][19] = 0;
+ this._roundData[5][21] = 0;
+ this._roundData[5][23] = 0;
+};
+
+Neo.Painter.prototype._initToneData = function() {
+ var pattern = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
+
+ for (var i = 0; i < 16; i++) {
+ this._toneData[i] = new Uint8Array(16);
+ for (var j = 0; j < 16; j++) {
+ this._toneData[i][j] = (i >= pattern[j]) ? 1 : 0;
+ }
+ }
+};
+
+Neo.Painter.prototype.getToneData = function(alpha) {
+ var alphaTable = [23,
+ 47,
+ 69,
+ 92,
+ 114,
+ 114,
+ 114,
+ 138,
+ 161,
+ 184,
+ 184,
+ 207,
+ 230,
+ 230,
+ 253,
+ ];
+
+ for (var i = 0; i < alphaTable.length; i++) {
+ if (alpha < alphaTable[i]) {
+ return this._toneData[i];
+ }
+ }
+ return this._toneData[i];
+};
+
+Neo.Painter.prototype._initInputText = function() {
+ var text = document.getElementById("inputtext");
+ if (!text) {
+ text = document.createElement("div");
+ }
+
+ text.id = "inputext";
+ text.setAttribute("contentEditable", true);
+ text.spellcheck = false;
+ text.className = "inputText";
+ text.innerHTML = "";
+
+ text.style.display = "none";
+// text.style.userSelect = "none";
+ Neo.painter.container.appendChild(text);
+ this.inputText = text;
+
+ this.updateInputText();
+};
+
+Neo.Painter.prototype.hideInputText = function() {
+ var text = this.inputText;
+ text.blur();
+ text.style.display = "none";
+};
+
+Neo.Painter.prototype.updateInputText = function() {
+ var text = this.inputText;
+ var d = this.lineWidth;
+ var fontSize = Math.round(d * 55/28 + 7);
+ var height = Math.round(d * 68/28 + 12);
+
+ text.style.fontSize = fontSize + "px";
+ text.style.lineHeight = fontSize + "px";
+ text.style.height = fontSize + "px";
+ text.style.marginTop = -fontSize + "px";
+};
+
+/*
+-----------------------------------------------------------------------
+ Mouse Event Handling
+-----------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype._keyDownHandler = function(e) {
+ this.isShiftDown = e.shiftKey;
+ this.isCtrlDown = e.ctrlKey;
+ this.isAltDown = e.altKey;
+ if (e.keyCode == 32) this.isSpaceDown = true;
+
+ if (!this.isShiftDown && this.isCtrlDown) {
+ if (!this.isAltDown) {
+ if (e.keyCode == 90 || e.keyCode == 85) this.undo(); //Ctrl+Z,Ctrl.U
+ if (e.keyCode == 89) this.redo(); //Ctrl+Y
+ } else {
+ if (e.keyCode == 90) this.redo(); //Ctrl+Alt+Z
+ }
+ }
+
+ if (!this.isShiftDown && !this.isCtrlDown && !this.isAltDown) {
+ if (e.keyCode == 107) new Neo.ZoomPlusCommand(this).execute(); // +
+ if (e.keyCode == 109) new Neo.ZoomMinusCommand(this).execute(); // -
+ }
+
+ if (this.tool.keyDownHandler) {
+ this.tool.keyDownHandler(e);
+ }
+
+ //スペース・Shift+スペースã§ã§ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã—ãªã„よã†ã«
+ if (document.activeElement != this.inputText) e.preventDefault();
+};
+
+Neo.Painter.prototype._keyUpHandler = function(e) {
+ this.isShiftDown = e.shiftKey;
+ this.isCtrlDown = e.ctrlKey;
+ this.isAltDown = e.altKey;
+ if (e.keyCode == 32) this.isSpaceDown = false;
+
+ if (this.tool.keyUpHandler) {
+ this.tool.keyUpHandler(oe);
+ }
+};
+
+Neo.Painter.prototype._rollOverHandler = function(e) {
+ if (this.tool.rollOverHandler) {
+ this.tool.rollOverHandler(this);
+ }
+};
+
+Neo.Painter.prototype._rollOutHandler = function(e) {
+ if (this.tool.rollOutHandler) {
+ this.tool.rollOutHandler(this);
+ }
+};
+
+Neo.Painter.prototype._mouseDownHandler = function(e) {
+ if (e.target == Neo.painter.destCanvas) {
+ //よãã‚ã‹ã‚‰ãªã„ãŒChromeã§ãƒ‰ãƒ©ãƒƒã‚°ã®æ™‚カレットãŒå‡ºã‚‹ã®ã‚’防ã
+ //http://stackoverflow.com/questions/2745028/chrome-sets-cursor-to-text-while-dragging-why
+ e.preventDefault();
+ }
+
+ if (e.button == 2) {
+ this.isMouseDownRight = true;
+
+ } else {
+ if (!e.shiftKey && e.ctrlKey && e.altKey) {
+ this.isMouseDown = true;
+
+ } else {
+ if (e.ctrlKey || e.altKey) {
+ this.isMouseDownRight = true;
+ } else {
+ this.isMouseDown = true;
+ }
+ }
+ }
+
+ this._updateMousePosition(e);
+ this.prevMouseX = this.mouseX;
+ this.prevMouseY = this.mouseY;
+
+ if (this.isMouseDownRight) {
+ this.isMouseDownRight = false;
+ if (!this.isWidget(e.target)) {
+ this.pickColor(this.mouseX, this.mouseY);
+ return;
+ }
+ }
+
+ if (!this.isUIPaused()) {
+ if (e.target['data-bar']) {
+ this.pushTool(new Neo.HandTool());
+
+ } else if (this.isSpaceDown && document.activeElement != this.inputText) {
+ this.pushTool(new Neo.HandTool());
+ this.tool.reverse = true;
+
+ } else if (e.target['data-slider'] != undefined) {
+ this.pushTool(new Neo.SliderTool());
+ this.tool.target = e.target;
+
+ } else if (e.ctrlKey && e.altKey && !e.shiftKey) {
+ this.pushTool(new Neo.SliderTool());
+ this.tool.target = Neo.sliders[Neo.SLIDERTYPE_SIZE].element;
+ this.tool.alt = true;
+
+ } else if (this.isWidget(e.target)) {
+ this.isMouseDown = false;
+ this.pushTool(new Neo.DummyTool());
+ }
+ }
+ this.tool.downHandler(this);
+
+ var ref = this;
+ document.onmouseup = function(e) {
+ ref._mouseUpHandler(e)
+ };
+};
+
+Neo.Painter.prototype._mouseUpHandler = function(e) {
+ this.isMouseDown = false;
+ this.isMouseDownRight = false;
+ this.tool.upHandler(this);
+ document.onmouseup = undefined;
+};
+
+Neo.Painter.prototype._mouseMoveHandler = function(e) {
+ this._updateMousePosition(e);
+
+ if (this.isMouseDown || this.isMouseDownRight) {
+ this.tool.moveHandler(this);
+ } else {
+ if (this.tool.upMoveHandler) {
+ this.tool.upMoveHandler(this);
+ }
+ }
+ this.prevMouseX = this.mouseX;
+ this.prevMouseY = this.mouseY;
+};
+
+
+Neo.Painter.prototype._updateMousePosition = function(e) {
+ var rect = this.destCanvas.getBoundingClientRect();
+ var x = (e.clientX !== undefined) ? e.clientX : e.touches[0].clientX;
+ var y = (e.clientY !== undefined) ? e.clientY : e.touches[0].clientY;
+
+ if (this.zoom <= 0) this.zoom = 1; //ãªãœã‹0ã«ãªã‚‹ã“ã¨ãŒã‚ã‚‹ã®ã§
+
+ this.mouseX = (x - rect.left) / this.zoom
+ + this.zoomX
+ - this.destCanvas.width * 0.5 / this.zoom;
+ this.mouseY = (y - rect.top) / this.zoom
+ + this.zoomY
+ - this.destCanvas.height * 0.5 / this.zoom;
+
+ if (isNaN(this.prevMouseX)) {
+ this.prevMouseX = this.mouseX;
+ }
+ if (isNaN(this.prevMouseY)) {
+ this.prevMosueY = this.mouseY;
+ }
+
+ this.slowX = this.slowX * 0.8 + this.mouseX * 0.2;
+ this.slowY = this.slowY * 0.8 + this.mouseY * 0.2;
+ var now = new Date().getTime();
+ if (this.stab) {
+ var pause = this.stab[3];
+ if (pause) {
+ // ãƒãƒ¼ã‚ºä¸­
+ if (now > pause) {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+
+ } else {
+ // ãƒãƒ¼ã‚ºã•ã‚Œã¦ã„ãªã„ã¨ã
+ var prev = this.stab[2];
+ if (now - prev > 150) { // 150ms以上止ã¾ã£ã¦ã„ãŸã‚‰ãƒãƒ¼ã‚ºã‚’オンã«ã™ã‚‹
+ this.stab[3] = now + 200 // 200msペンã®ä½ç½®ã‚’固定
+
+ } else {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+ }
+ } else {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+
+ this.rawMouseX = x;
+ this.rawMouseY = y;
+ this.clipMouseX = Math.max(Math.min(this.canvasWidth, this.mouseX), 0);
+ this.clipMouseY = Math.max(Math.min(this.canvasHeight, this.mouseY), 0);
+};
+
+Neo.Painter.prototype._beforeUnloadHandler = function(e) {
+ // quick save
+};
+
+Neo.Painter.prototype.getStabilized = function() {
+ return this.stab;
+};
+
+/*
+-------------------------------------------------------------------------
+ Undo
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.undo = function() {
+ var undoItem = this._undoMgr.popUndo();
+ if (undoItem) {
+ this._pushRedo();
+ this.canvasCtx[0].putImageData(undoItem.data[0], undoItem.x,undoItem.y);
+ this.canvasCtx[1].putImageData(undoItem.data[1], undoItem.x,undoItem.y);
+ this.updateDestCanvas(undoItem.x, undoItem.y, undoItem.width, undoItem.height);
+ }
+};
+
+Neo.Painter.prototype.redo = function() {
+ var undoItem = this._undoMgr.popRedo();
+ if (undoItem) {
+ this._pushUndo(0,0,this.canvasWidth, this.canvasHeight, true);
+ this.canvasCtx[0].putImageData(undoItem.data[0], undoItem.x,undoItem.y);
+ this.canvasCtx[1].putImageData(undoItem.data[1], undoItem.x,undoItem.y);
+ this.updateDestCanvas(undoItem.x, undoItem.y, undoItem.width, undoItem.height);
+ }
+};
+
+Neo.Painter.prototype.hasUndo = function() {
+ return true;
+};
+
+Neo.Painter.prototype._pushUndo = function(x, y, w, h, holdRedo) {
+ x = (x == undefined) ? 0 : x;
+ y = (y == undefined) ? 0 : y;
+ w = (w == undefined) ? this.canvasWidth : w;
+ h = (h == undefined) ? this.canvasHeight : h;
+ var undoItem = new Neo.UndoItem();
+ undoItem.x = 0;
+ undoItem.y = 0;
+ undoItem.width = w;
+ undoItem.height = h;
+ undoItem.data = [this.canvasCtx[0].getImageData(x, y, w, h),
+ this.canvasCtx[1].getImageData(x, y, w, h)];
+ this._undoMgr.pushUndo(undoItem, holdRedo);
+};
+
+Neo.Painter.prototype._pushRedo = function(x, y, w, h) {
+ x = (x == undefined) ? 0 : x;
+ y = (y == undefined) ? 0 : y;
+ w = (w == undefined) ? this.canvasWidth : w;
+ h = (h == undefined) ? this.canvasHeight : h;
+ var undoItem = new Neo.UndoItem();
+ undoItem.x = 0;
+ undoItem.y = 0;
+ undoItem.width = w;
+ undoItem.height = h;
+ undoItem.data = [this.canvasCtx[0].getImageData(x, y, w, h),
+ this.canvasCtx[1].getImageData(x, y, w, h)];
+ this._undoMgr.pushRedo(undoItem);
+};
+
+
+/*
+-------------------------------------------------------------------------
+ Data Cache for Undo / Redo
+-------------------------------------------------------------------------
+*/
+
+Neo.UndoManager = function(_maxStep){
+ this._maxStep = _maxStep;
+ this._undoItems = [];
+ this._redoItems = [];
+}
+Neo.UndoManager.prototype._maxStep;
+Neo.UndoManager.prototype._redoItems;
+Neo.UndoManager.prototype._undoItems;
+
+//アクションをã—ã¦Undo情報を更新
+Neo.UndoManager.prototype.pushUndo = function(undoItem, holdRedo) {
+ this._undoItems.push(undoItem);
+ if (this._undoItems.length > this._maxStep) {
+ this._undoItems.shift();
+ }
+
+ if (!holdRedo == true) {
+ this._redoItems = [];
+ }
+};
+
+Neo.UndoManager.prototype.popUndo = function() {
+ return this._undoItems.pop();
+}
+
+Neo.UndoManager.prototype.pushRedo = function(undoItem) {
+ this._redoItems.push(undoItem);
+}
+
+Neo.UndoManager.prototype.popRedo = function() {
+ return this._redoItems.pop();
+}
+
+
+Neo.UndoItem = function() {}
+Neo.UndoItem.prototype.data;
+Neo.UndoItem.prototype.x;
+Neo.UndoItem.prototype.y;
+Neo.UndoItem.prototype.width;
+Neo.UndoItem.prototype.height;
+
+/*
+-------------------------------------------------------------------------
+ Zoom Controller
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.setZoom = function(value) {
+ this.zoom = value;
+
+ var container = document.getElementById("container");
+ var width = this.canvasWidth * this.zoom;
+ var height = this.canvasHeight * this.zoom;
+ if (width > container.clientWidth - 100) width = container.clientWidth - 100;
+ if (height > container.clientHeight - 130) height = container.clientHeight - 130;
+ this.destWidth = width;
+ this.destHeight = height;
+
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight, false);
+ this.setZoomPosition(this.zoomX, this.zoomY);
+};
+
+Neo.Painter.prototype.setZoomPosition = function(x, y) {
+ var minx = (this.destCanvas.width / this.zoom) * 0.5;
+ var maxx = this.canvasWidth - minx;
+ var miny = (this.destCanvas.height / this.zoom) * 0.5;
+ var maxy = this.canvasHeight - miny;
+
+
+ x = Math.round(Math.max(Math.min(maxx,x),minx));
+ y = Math.round(Math.max(Math.min(maxy,y),miny));
+
+ this.zoomX = x;
+ this.zoomY = y;
+ this.updateDestCanvas(0,0,this.canvasWidth,this.canvasHeight,false);
+
+ this.scrollBarX = (maxx == minx) ? 0 : (x - minx) / (maxx - minx);
+ this.scrollBarY = (maxy == miny) ? 0 : (y - miny) / (maxy - miny);
+ this.scrollWidth = maxx - minx;
+ this.scrollHeight = maxy - miny;
+
+ if (Neo.scrollH) Neo.scrollH.update(this);
+ if (Neo.scrollV) Neo.scrollV.update(this);
+
+ this.hideInputText();
+};
+
+
+/*
+-------------------------------------------------------------------------
+ Drawing Helper
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.submit = function(board) {
+ var thumbnail = null;
+ var thumbnail2 = null;
+
+ if (this.useThumbnail()) {
+ thumbnail = this.getThumbnail(Neo.config.thumbnail_type || "png");
+ if (Neo.config.thumbnail_type2) {
+ thumbnail2 = this.getThumbnail(Neo.config.thumbnail_type2);
+ }
+ }
+ Neo.submit(board, this.getPNG(), thumbnail2, thumbnail);
+};
+
+Neo.Painter.prototype.useThumbnail = function() {
+ var thumbnailWidth = this.getThumbnailWidth();
+ var thumbnailHeight = this.getThumbnailHeight();
+ if (thumbnailWidth && thumbnailHeight) {
+ if (thumbnailWidth < this.canvasWidth ||
+ thumbnailHeight < this.canvasHeight) {
+ return true;
+ }
+ }
+ return false;
+};
+
+Neo.Painter.prototype.dataURLtoBlob = function(dataURL) {
+ var byteString;
+ if (dataURL.split(',')[0].indexOf('base64') >= 0) {
+ byteString = atob(dataURL.split(',')[1]);
+ } else {
+ byteString = unescape(dataURL.split(',')[1]);
+ }
+
+ // write the bytes of the string to a typed array
+ var ia = new Uint8Array(byteString.length);
+ for (var i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
+ }
+ return new Blob([ia], {type:'image/png'});
+};
+
+Neo.Painter.prototype.getImage = function(imageWidth, imageHeight) {
+ var width = this.canvasWidth;
+ var height = this.canvasHeight;
+ imageWidth = imageWidth || width;
+ imageHeight = imageHeight || height;
+
+ var pngCanvas = document.createElement("canvas");
+ pngCanvas.width = imageWidth;
+ pngCanvas.height = imageHeight;
+ var pngCanvasCtx = pngCanvas.getContext("2d");
+ pngCanvasCtx.fillStyle = "#ffffff";
+ pngCanvasCtx.fillRect(0, 0, imageWidth, imageHeight);
+
+ if (this.visible[0]) {
+ pngCanvasCtx.drawImage(this.canvas[0],
+ 0, 0, width, height,
+ 0, 0, imageWidth, imageHeight);
+ }
+ if (this.visible[1]) {
+ pngCanvasCtx.drawImage(this.canvas[1],
+ 0, 0, width, height,
+ 0, 0, imageWidth, imageHeight);
+ }
+ return pngCanvas;
+};
+
+Neo.Painter.prototype.getPNG = function() {
+ var image = this.getImage();
+ var dataURL = image.toDataURL('image/png');
+ return this.dataURLtoBlob(dataURL);
+};
+
+Neo.Painter.prototype.getThumbnail = function(type) {
+ if (type != "animation") {
+ var thumbnailWidth = this.getThumbnailWidth();
+ var thumbnailHeight = this.getThumbnailHeight();
+ if (thumbnailWidth || thumbnailHeight) {
+ var width = this.canvasWidth;
+ var height = this.canvasHeight;
+ if (thumbnailWidth == 0) {
+ thumbnailWidth = thumbnailHeight * width / height;
+ }
+ if (thumbnailHeight == 0) {
+ thumbnailHeight = thumbnailWidth * height / width;
+ }
+ } else {
+ thumbnailWidth = thumbnailHeight = null;
+ }
+
+ console.log("get thumbnail", thumbnailWidth, thumbnailHeight);
+
+ var image = this.getImage(thumbnailWidth, thumbnailHeight);
+ var dataURL = image.toDataURL('image/' + type);
+ return this.dataURLtoBlob(dataURL);
+
+ } else {
+ return new Blob([]); //animationã«ã¯å¯¾å¿œã—ã¦ã„ãªã„ã®ã§ãƒ€ãƒŸãƒ¼ãƒ‡ãƒ¼ã‚¿ã‚’è¿”ã™
+ }
+};
+
+Neo.Painter.prototype.getThumbnailWidth = function() {
+ var width = Neo.config.thumbnail_width;
+ if (width) {
+ if (width.match(/%$/)) {
+ return Math.floor(this.canvasWidth * (parseInt(width) / 100.0));
+ } else {
+ return parseInt(width);
+ }
+ }
+ return 0;
+};
+
+Neo.Painter.prototype.getThumbnailHeight = function() {
+ var height = Neo.config.thumbnail_height;
+ if (height) {
+ if (height.match(/%$/)) {
+ return Math.floor(this.canvasHeight * (parseInt(height) / 100.0));
+ } else {
+ return parseInt(height);
+ }
+ }
+ return 0;
+};
+
+Neo.Painter.prototype.clearCanvas = function(doConfirm) {
+ if (!doConfirm || confirm("Borrar todo")) {
+ //Register undo first;
+ this._pushUndo();
+
+ this.canvasCtx[0].clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ this.canvasCtx[1].clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+ }
+};
+
+Neo.Painter.prototype.updateDestCanvas = function(x, y, width, height, useTemp) {
+ var canvasWidth = this.canvasWidth;
+ var canvasHeight = this.canvasHeight;
+ var updateAll = false;
+ if (x == 0 && y == 0 && width == canvasWidth && height == canvasHeight) {
+ updateAll = true;
+ };
+
+ if (x + width > this.canvasWidth) width = this.canvasWidth - x;
+ if (y + height > this.canvasHeight) height = this.canvasHeight - y;
+ if (x < 0) x = 0;
+ if (y < 0) y = 0;
+ if (width <= 0 || height <= 0) return;
+
+ var ctx = this.destCanvasCtx;
+ ctx.save();
+ ctx.fillStyle = "#ffffff";
+
+ if (updateAll) {
+ ctx.fillRect(0, 0, this.destCanvas.width, this.destCanvas.height);
+ } else {
+ //カーソルã®æ画ゴミãŒæ®‹ã‚‹ã®ã‚’ã”ã¾ã‹ã™ãŸã‚
+ if (x + width == this.canvasWidth) width++;
+ if (y + height == this.canvasHeight) height++;
+ }
+
+ ctx.translate(this.destCanvas.width*.5, this.destCanvas.height*.5);
+ ctx.scale(this.zoom, this.zoom);
+ ctx.translate(-this.zoomX, -this.zoomY);
+ ctx.globalAlpha = 1.0;
+ ctx.msImageSmoothingEnabled = 0;
+
+ if (!updateAll) {
+ ctx.fillRect(x, y, width, height);
+ }
+
+ if (this.visible[0]) {
+ ctx.drawImage(this.canvas[0],
+ x, y, width, height,
+ x, y, width, height);
+ }
+ if (this.visible[1]) {
+ ctx.drawImage(this.canvas[1],
+ x, y, width, height,
+ x, y, width, height);
+ }
+ if (useTemp) {
+ ctx.globalAlpha = 1.0; //this.alpha;
+ ctx.drawImage(this.tempCanvas,
+ x, y, width, height,
+ x + this.tempX, y + this.tempY, width, height);
+ }
+ ctx.restore();
+};
+
+Neo.Painter.prototype.getBound = function(x0, y0, x1, y1, r) {
+ var left = Math.floor((x0 < x1) ? x0 : x1);
+ var top = Math.floor((y0 < y1) ? y0 : y1);
+ var width = Math.ceil(Math.abs(x0 - x1));
+ var height = Math.ceil(Math.abs(y0 - y1));
+ r = Math.ceil(r + 1);
+
+ if (!r) {
+ width += 1;
+ height += 1;
+
+ } else {
+ left -= r;
+ top -= r;
+ width += r * 2;
+ height += r * 2;
+ }
+ return [left, top, width, height];
+};
+
+Neo.Painter.prototype.getColor = function(c) {
+ if (!c) c = this.foregroundColor;
+ var r = parseInt(c.substr(1, 2), 16);
+ var g = parseInt(c.substr(3, 2), 16);
+ var b = parseInt(c.substr(5, 2), 16);
+ var a = Math.floor(this.alpha * 255);
+ return a <<24 | b<<16 | g<<8 | r;
+};
+
+Neo.Painter.prototype.getColorString = function(c) {
+ var rgb = ("000000" + (c & 0xffffff).toString(16)).substr(-6);
+ return '#' + rgb;
+};
+
+Neo.Painter.prototype.setColor = function(c) {
+ if (typeof c != "string") c = this.getColorString(c);
+ this.foregroundColor = c;
+
+ Neo.updateUI();
+};
+
+Neo.Painter.prototype.getAlpha = function(type) {
+ var a1 = this.alpha;
+
+ switch (type) {
+ case Neo.Painter.ALPHATYPE_PEN:
+ if (a1 > 0.5) {
+ a1 = 1.0/16 + (a1 - 0.5) * 30.0/16;
+ } else {
+ a1 = Math.sqrt(2 * a1) / 16.0;
+ }
+ a1 = Math.min(1, Math.max(0, a1));
+ break;
+
+ case Neo.Painter.ALPHATYPE_FILL:
+ a1 = -0.00056 * a1 + 0.0042 / (1.0 - a1) - 0.0042;
+ a1 = Math.min(1.0, Math.max(0, a1 * 10));
+ break;
+
+ case Neo.Painter.ALPHATYPE_BRUSH:
+ a1 = -0.00056 * a1 + 0.0042 / (1.0 - a1) - 0.0042;
+ a1 = Math.min(1.0, Math.max(0, a1));
+ break;
+ }
+
+ // アルファãŒå°ã•ã„時ã¯é©å½“ã«ç‚¹ã‚’抜ã„ã¦è¦‹ãŸç›®ã®æ¿ƒåº¦ã‚’åˆã‚ã›ã‚‹
+ if (a1 < 1.0/255) {
+ this.aerr += a1;
+ a1 = 0;
+ while (this.aerr > 1.0/255) {
+ a1 = 1.0/255;
+ this.aerr -= 1.0/255;
+ }
+ }
+ return a1;
+};
+
+Neo.Painter.prototype.prepareDrawing = function () {
+ var r = parseInt(this.foregroundColor.substr(1, 2), 16);
+ var g = parseInt(this.foregroundColor.substr(3, 2), 16);
+ var b = parseInt(this.foregroundColor.substr(5, 2), 16);
+ var a = Math.floor(this.alpha * 255);
+
+ var maskR = parseInt(this.maskColor.substr(1, 2), 16);
+ var maskG = parseInt(this.maskColor.substr(3, 2), 16);
+ var maskB = parseInt(this.maskColor.substr(5, 2), 16);
+
+ this._currentColor = [r, g, b, a];
+ this._currentMask = [maskR, maskG, maskB];
+};
+
+Neo.Painter.prototype.isMasked = function (buf8, index) {
+ var r = this._currentMask[0];
+ var g = this._currentMask[1];
+ var b = this._currentMask[2];
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3];
+
+ if (a0 == 0) {
+ r0 = 0xff;
+ g0 = 0xff;
+ b0 = 0xff;
+ }
+
+ var type = this.maskType;
+
+ //TODO
+ //ã„ã‚ã„ã‚試ã—ãŸã®ã§ã™ãŒåŠé€æ˜Žã§æç”»ã™ã‚‹ã¨ãã®åŠ ç®—・逆加算をå†ç¾ã™ã‚‹æ–¹æ³•ãŒã‚ã‹ã‚Šã¾ã›ã‚“。
+ //ã¨ã‚Šã‚ãˆãšå˜ç´”ã«ç„¡è¦–ã—ã¦ã„ã¾ã™ã€‚
+ if (type == Neo.Painter.MASKTYPE_ADD ||
+ type == Neo.Painter.MASKTYPE_SUB) {
+ if (this._currentColor[3] < 250) {
+ type = Neo.Painter.MASKTYPE_NONE;
+ }
+ }
+
+ switch (type) {
+ case Neo.Painter.MASKTYPE_NONE:
+ return;
+
+ case Neo.Painter.MASKTYPE_NORMAL:
+ return (r0 == r &&
+ g0 == g &&
+ b0 == b) ? true : false;
+
+ case Neo.Painter.MASKTYPE_REVERSE:
+ return (r0 != r ||
+ g0 != g ||
+ b0 != b) ? true : false;
+
+ case Neo.Painter.MASKTYPE_ADD:
+ if (a0 > 0) {
+ var sort = this.sortColor(r0, g0, b0);
+ for (var i = 0; i < 3; i++) {
+ var c = sort[i];
+ if (buf8[index + c] < this._currentColor[c]) return true;
+ }
+ return false;
+
+ } else {
+ return false;
+ }
+
+ case Neo.Painter.MASKTYPE_SUB:
+ if (a0 > 0) {
+ var sort = this.sortColor(r0, g0, b0);
+ for (var i = 0; i < 3; i++) {
+ var c = sort[i];
+ if (buf8[index + c] > this._currentColor[c]) return true;
+ }
+ return false;
+ } else {
+ return true;
+ }
+ }
+};
+
+Neo.Painter.prototype.setPoint = function(buf8, bufWidth, x0, y0, left, top, type) {
+ var x = x0 - left;
+ var y = y0 - top;
+
+ switch (type) {
+ case Neo.Painter.LINETYPE_PEN:
+ this.setPenPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BRUSH:
+ this.setBrushPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_TONE:
+ this.setTonePoint(buf8, bufWidth, x, y, x0, y0);
+ break;
+
+ case Neo.Painter.LINETYPE_ERASER:
+ this.setEraserPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BLUR:
+ this.setBlurPoint(buf8, bufWidth, x, y, x0, y0);
+ break;
+
+ case Neo.Painter.LINETYPE_DODGE:
+ this.setDodgePoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BURN:
+ this.setBurnPoint(buf8, bufWidth, x, y);
+ break;
+
+ default:
+ break;
+ }
+};
+
+
+Neo.Painter.prototype.setPenPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_PEN);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var a1x = Math.max(a1, 1.0/255);
+
+ var r = (r1 * a1x + r0 * a0 * (1 - a1x)) / a;
+ var g = (g1 * a1x + g0 * a0 * (1 - a1x)) / a;
+ var b = (b1 * a1x + b0 * a0 * (1 - a1x)) / a;
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBrushPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var a1x = Math.max(a1, 1.0/255);
+
+ var r = (r1 * a1x + r0 * a0) / (a0 + a1x);
+ var g = (g1 * a1x + g0 * a0) / (a0 + a1x);
+ var b = (b1 * a1x + b0 * a0) / (a0 + a1x);
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setTonePoint = function(buf8, width, x, y, x0, y0) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ if (r0%2) { x0++; y0++; } //ãªãœã‹æ¨¡æ§˜ãŒãšã‚Œã‚‹ã®ã§
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var index = (y * width + x) * 4;
+
+ var r = this._currentColor[0];
+ var g = this._currentColor[1];
+ var b = this._currentColor[2];
+ var a = this._currentColor[3];
+
+ var toneData = this.getToneData(a);
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ if (toneData[((y0+i)%4) + (((x0+j)%4) * 4)]) {
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = 255;
+ }
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setEraserPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var index = (y * width + x) * 4;
+ var a = Math.floor(this.alpha * 255);
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var k = (buf8[index + 3] / 255.0) * (1.0 - (a / 255.0));
+
+ buf8[index + 3] -= a / (d * (255.0 - a) / 255.0);
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBlurPoint = function(buf8, width, x, y, x0, y0) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var height = buf8.length / (width * 4);
+
+// var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ var a1 = this.alpha / 12;
+ if (a1 == 0) return;
+ var blur = a1;
+
+ var tmp = new Uint8ClampedArray(buf8.length);
+ for (var i = 0; i < buf8.length; i++) {
+ tmp[i] = buf8[i];
+ }
+
+ var left = x0 - x - r0;
+ var top = y0 - y - r0;
+
+ var xstart = 0, xend = d;
+ var ystart = 0, yend = d;
+ if (xstart > left) xstart = -left;
+ if (ystart > top) ystart = -top;
+ if (xend > this.canvasWidth - left) xend = this.canvasWidth - left;
+ if (yend > this.canvasHeight - top) yend = this.canvasHeight - top;
+
+ for (var j = ystart; j < yend; j++) {
+ var index = (j * width + xstart) * 4;
+ for (var i = xstart; i < xend; i++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var rgba = [0, 0, 0, 0, 0];
+
+ this.addBlur(tmp, index, 1.0 - blur*4, rgba);
+ if (i > xstart) this.addBlur(tmp, index - 4, blur, rgba);
+ if (i < xend - 1) this.addBlur(tmp, index + 4, blur, rgba);
+ if (j > ystart) this.addBlur(tmp, index - width*4, blur, rgba);
+ if (j < yend - 1) this.addBlur(tmp, index + width*4, blur, rgba);
+
+ buf8[index + 0] = Math.round(rgba[0]);
+ buf8[index + 1] = Math.round(rgba[1]);
+ buf8[index + 2] = Math.round(rgba[2]);
+ buf8[index + 3] = Math.round((rgba[3] / rgba[4]) * 255.0);
+ }
+ index += 4;
+ }
+ }
+};
+
+Neo.Painter.prototype.setDodgePoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ if (a1 != 255.0) {
+ var r1 = r0 * 255 / (255 - a1);
+ var g1 = g0 * 255 / (255 - a1);
+ var b1 = b0 * 255 / (255 - a1);
+ } else {
+ var r1 = 255.0;
+ var g1 = 255.0;
+ var b1 = 255.0;
+ }
+
+ var r = Math.ceil(r1);
+ var g = Math.ceil(g1);
+ var b = Math.ceil(b1);
+ var a = a0;
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBurnPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ if (a1 != 255.0) {
+ var r1 = 255 - (255 - r0) * 255 / (255 - a1);
+ var g1 = 255 - (255 - g0) * 255 / (255 - a1);
+ var b1 = 255 - (255 - b0) * 255 / (255 - a1);
+ } else {
+ var r1 = 0;
+ var g1 = 0;
+ var b1 = 0;
+ }
+
+ var r = Math.floor(r1);
+ var g = Math.floor(g1);
+ var b = Math.floor(b1);
+ var a = a0;
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////
+
+Neo.Painter.prototype.xorPixel = function(buf32, bufWidth, x, y, c) {
+ var index = y * bufWidth + x;
+ if (!c) c = 0xffffff;
+ buf32[index] ^= c;
+};
+
+Neo.Painter.prototype.getBezierPoint = function(t, x0, y0, x1, y1, x2, y2, x3, y3) {
+ var a0 = (1 - t) * (1 - t) * (1 - t);
+ var a1 = (1 - t) * (1 - t) * t * 3;
+ var a2 = (1 - t) * t * t * 3;
+ var a3 = t * t * t;
+
+ var x = x0 * a0 + x1 * a1 + x2 * a2 + x3 * a3;
+ var y = y0 * a0 + y1 * a1 + y2 * a2 + y3 * a3;
+ return [x, y];
+};
+
+var nmax = 1;
+
+Neo.Painter.prototype.drawBezier = function(ctx, x0, y0, x1, y1, x2, y2, x3, y3, type) {
+ var xmax = Math.max(x0, x1, x2, x3);
+ var xmin = Math.min(x0, x1, x2, x3);
+ var ymax = Math.max(y0, y1, y2, y3);
+ var ymin = Math.min(y0, y1, y2, y3);
+ var n = Math.ceil(((xmax - xmin) + (ymax - ymin)) * 2.5);
+
+ if (n > nmax) {
+ n = (n < nmax * 2) ? n : nmax * 2;
+ nmax = n;
+ }
+
+ for (var i = 0; i < n; i++) {
+ var t = i * 1.0 / n;
+ var p = this.getBezierPoint(t, x0, y0, x1, y1, x2, y2, x3, y3);
+ this.drawPoint(ctx, p[0], p[1], type);
+ }
+};
+
+Neo.Painter.prototype.prevLine = null; // 始点ã¾ãŸã¯çµ‚点ãŒ2度プロットã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹ã®ã§
+Neo.Painter.prototype.drawLine = function(ctx, x0, y0, x1, y1, type) {
+ x0 = Math.round(x0);
+ x1 = Math.round(x1);
+ y0 = Math.round(y0);
+ y1 = Math.round(y1);
+ var prev = [x0, y0, x1, y1];
+
+ var width = Math.abs(x1 - x0);
+ var height = Math.abs(y1 - y0);
+ var r = Math.ceil(this.lineWidth / 2);
+
+ var left = ((x0 < x1) ? x0 : x1) - r;
+ var top = ((y0 < y1) ? y0 : y1) - r;
+
+ var imageData = ctx.getImageData(left, top, width + r*2, height + r*2);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var dx = width, sx = x0 < x1 ? 1 : -1;
+ var dy = height, sy = y0 < y1 ? 1 : -1;
+ var err = (dx > dy ? dx : -dy) / 2;
+ this.aerr = 0;
+
+ while (true) {
+ if (this.prevLine == null ||
+ !((this.prevLine[0] == x0 && this.prevLine[1] == y0) ||
+ (this.prevLine[2] == x0 && this.prevLine[3] == y0))) {
+ this.setPoint(buf8, imageData.width, x0, y0, left, top, type);
+ }
+
+ if (x0 === x1 && y0 === y1) break;
+ var e2 = err;
+ if (e2 > -dx) { err -= dy; x0 += sx; }
+ if (e2 < dy) { err += dx; y0 += sy; }
+ }
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, left, top);
+
+ this.prevLine = prev;
+};
+
+Neo.Painter.prototype.drawPoint = function(ctx, x, y, type) {
+ this.drawLine(ctx, x, y, x, y, type);
+};
+
+Neo.Painter.prototype.xorRect = function(buf32, bufWidth, x, y, width, height, c) {
+ var index = y * bufWidth + x;
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ buf32[index] ^= c;
+ index++;
+ }
+ index += width - bufWidth;
+ }
+};
+
+Neo.Painter.prototype.drawXORRect = function(ctx, x, y, width, height, isFill, c) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+ if (width == 0 || height == 0) return;
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var index = 0;
+ if (!c) c = 0xffffff;
+
+ if (isFill) {
+ this.xorRect(buf32, width, 0, 0, width, height, c);
+
+ } else {
+ for (var i = 0; i < width; i++) { //top
+ buf32[index] = buf32[index] ^= c;
+ index++;
+ }
+ if (height > 1) {
+ index = width;
+ for (var i = 1; i < height; i++) { //left
+ buf32[index] = buf32[index] ^= c;
+ index += width;
+ }
+ if (width > 1) {
+ index = width * 2 - 1;
+ for (var i = 1; i < height - 1; i++) { //right
+ buf32[index] = buf32[index] ^= c;
+ index += width;
+ }
+ index = width * (height - 1) + 1;
+ for (var i = 1; i < width; i++) { // bottom
+ buf32[index] = buf32[index] ^= c;
+ index++;
+ }
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.drawXOREllipse = function(ctx, x, y, width, height, isFill, c) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+ if (width == 0 || height == 0) return;
+ if (!c) c = 0xffffff;
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+
+ var a = width-1, b = height-1, b1 = b&1; /* values of diameter */
+ var dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */
+ var err = dx+dy+b1*a*a, e2; /* error of 1.step */
+
+ var x0 = x;
+ var y0 = y;
+ var x1 = x0+a;
+ var y1 = y0+b;
+
+ if (x0 > x1) { x0 = x1; x1 += a; }
+ if (y0 > y1) y0 = y1;
+ y0 += Math.floor((b+1)/2); y1 = y0-b1; /* starting pixel */
+ a *= 8*a; b1 = 8*b*b;
+ var ymin = y0 - 1;
+
+ do {
+ if (isFill) {
+ if (ymin < y0) {
+ this.xorRect(buf32, width, x0-x, y0 - y, x1 - x0, 1, c);
+ if (y0 != y1) {
+ this.xorRect(buf32, width, x0-x, y1 - y, x1 - x0, 1, c);
+ }
+ ymin = y0;
+ }
+ } else {
+ this.xorPixel(buf32, width, x1-x, y0-y, c);
+ if (x0 != x1) {
+ this.xorPixel(buf32, width, x0-x, y0-y, c);
+ }
+ if (y0 != y1) {
+ this.xorPixel(buf32, width, x0-x, y1-y, c);
+ if (x0 != x1) {
+ this.xorPixel(buf32, width, x1-x, y1-y, c);
+ }
+ }
+ }
+ e2 = 2*err;
+ if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */
+ if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */
+ } while (x0 <= x1);
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.drawXORLine = function(ctx, x0, y0, x1, y1, c) {
+ x0 = Math.round(x0);
+ x1 = Math.round(x1);
+ y0 = Math.round(y0);
+ y1 = Math.round(y1);
+
+ var width = Math.abs(x1 - x0);
+ var height = Math.abs(y1 - y0);
+
+ var left = ((x0 < x1) ? x0 : x1);
+ var top = ((y0 < y1) ? y0 : y1);
+// console.log("left:"+left+" top:"+top+" width:"+width+" height:"+height);
+
+ var imageData = ctx.getImageData(left, top, width + 1, height + 1);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var dx = width, sx = x0 < x1 ? 1 : -1;
+ var dy = height, sy = y0 < y1 ? 1 : -1;
+ var err = (dx > dy ? dx : -dy) / 2;
+
+ while (true) {
+ if (this.prevLine == null ||
+ !((this.prevLine[0] == x0 && this.prevLine[1] == y0) ||
+ (this.prevLine[2] == x0 && this.prevLine[3] == y0))) {
+
+ this.xorPixel(buf32, imageData.width, x0 - left, y0 - top, c);
+ }
+
+ if (x0 === x1 && y0 === y1) break;
+ var e2 = err;
+ if (e2 > -dx) { err -= dy; x0 += sx; }
+ if (e2 < dy) { err += dx; y0 += sy; }
+ }
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, left, top);
+};
+
+
+Neo.Painter.prototype.eraseRect = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var index = 0;
+
+ var a = 1.0 - this.alpha;
+ if (a != 0) {
+ a = Math.ceil(2.0 / a);
+ } else {
+ a = 255;
+ }
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ if (!this.isMasked(buf8, index)) {
+ buf8[index + 3] -= a;
+ }
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.flipH = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var half = Math.floor(width / 2);
+ for (var j = 0; j < height; j++) {
+ var index = j * width;
+ var index2 = index + (width - 1);
+ for (var i = 0; i < half; i++) {
+ var value = buf32[index + i];
+ buf32[index + i] = buf32[index2 -i];
+ buf32[index2 - i] = value;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.flipV = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var half = Math.floor(height / 2);
+ for (var j = 0; j < half; j++) {
+ var index = j * width;
+ var index2 = (height - 1 - j) * width;
+ for (var i = 0; i < width; i++) {
+ var value = buf32[index + i];
+ buf32[index + i] = buf32[index2 + i];
+ buf32[index2 + i] = value;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.merge = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = [];
+ var buf32 = [];
+ var buf8 = [];
+ for (var i = 0; i < 2; i++) {
+ imageData[i] = this.canvasCtx[i].getImageData(x, y, width, height);
+ buf32[i] = new Uint32Array(imageData[i].data.buffer);
+ buf8[i] = new Uint8ClampedArray(imageData[i].data.buffer);
+ }
+
+ var dst = this.current;
+ var src = (dst == 1) ? 0 : 1;
+ var size = width * height;
+ var index = 0;
+ for (var i = 0; i < size; i++) {
+ var r0 = buf8[0][index + 0];
+ var g0 = buf8[0][index + 1];
+ var b0 = buf8[0][index + 2];
+ var a0 = buf8[0][index + 3] / 255.0;
+ var r1 = buf8[1][index + 0];
+ var g1 = buf8[1][index + 1];
+ var b1 = buf8[1][index + 2];
+ var a1 = buf8[1][index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var r = Math.floor((r1 * a1 + r0 * a0 * (1 - a1)) / a + 0.5);
+ var g = Math.floor((g1 * a1 + g0 * a0 * (1 - a1)) / a + 0.5);
+ var b = Math.floor((b1 * a1 + b0 * a0 * (1 - a1)) / a + 0.5);
+ }
+ buf8[src][index + 0] = 0;
+ buf8[src][index + 1] = 0;
+ buf8[src][index + 2] = 0;
+ buf8[src][index + 3] = 0;
+ buf8[dst][index + 0] = r;
+ buf8[dst][index + 1] = g;
+ buf8[dst][index + 2] = b;
+ buf8[dst][index + 3] = Math.floor(a * 255 + 0.5);
+ index += 4;
+ }
+
+ for (var i = 0; i < 2; i++) {
+ imageData[i].data.set(buf8[i]);
+ this.canvasCtx[i].putImageData(imageData[i], x, y);
+ }
+};
+
+Neo.Painter.prototype.blurRect = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var tmp = new Uint8ClampedArray(buf8.length);
+ for (var i = 0; i < buf8.length; i++) tmp[i] = buf8[i];
+
+ var index = 0;
+ var a1 = this.alpha / 12;
+ var blur = a1;
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ var rgba = [0, 0, 0, 0, 0];
+
+ this.addBlur(tmp, index, 1.0 - blur*4, rgba);
+
+ if (i > 0) this.addBlur(tmp, index - 4, blur, rgba);
+ if (i < width - 1) this.addBlur(tmp, index + 4, blur, rgba);
+ if (j > 0) this.addBlur(tmp, index - width*4, blur, rgba);
+ if (j < height - 1) this.addBlur(tmp, index + width*4, blur, rgba);
+
+ var w = rgba[4];
+ buf8[index + 0] = Math.round(rgba[0]);
+ buf8[index + 1] = Math.round(rgba[1]);
+ buf8[index + 2] = Math.round(rgba[2]);
+ buf8[index + 3] = Math.ceil((rgba[3] / w) * 255.0);
+
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.addBlur = function(buffer, index, a, rgba) {
+ var r0 = rgba[0];
+ var g0 = rgba[1];
+ var b0 = rgba[2];
+ var a0 = rgba[3];
+ var r1 = buffer[index + 0];
+ var g1 = buffer[index + 1];
+ var b1 = buffer[index + 2];
+ var a1 = (buffer[index + 3] / 255.0) * a;
+ rgba[4] += a;
+
+ var a = a0 + a1;
+ if (a > 0) {
+ rgba[0] = (r1 * a1 + r0 * a0) / (a0 + a1);
+ rgba[1] = (g1 * a1 + g0 * a0) / (a0 + a1);
+ rgba[2] = (b1 * a1 + b0 * a0) / (a0 + a1);
+ rgba[3] = a;
+ }
+};
+
+Neo.Painter.prototype.pickColor = function(x, y) {
+ var r = 0xff, g = 0xff, b = 0xff, a;
+
+ x = Math.floor(x);
+ y = Math.floor(y);
+ if (x >= 0 && x < this.canvasWidth &&
+ y >= 0 && y < this.canvasHeight) {
+
+ for (var i = 0; i < 2; i++) {
+ if (this.visible[i]) {
+ var ctx = this.canvasCtx[i];
+ var imageData = ctx.getImageData(x, y, 1, 1);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var a = buf8[3] / 255.0;
+ r = r * (1.0 - a) + buf8[2] * a;
+ g = g * (1.0 - a) + buf8[1] * a;
+ b = b * (1.0 - a) + buf8[0] * a;
+ }
+ }
+ r = Math.max(Math.min(Math.round(r), 255), 0);
+ g = Math.max(Math.min(Math.round(g), 255), 0);
+ b = Math.max(Math.min(Math.round(b), 255), 0);
+ var result = r | g<<8 | b<<16;
+ }
+ this.setColor(result);
+
+
+ if (this.current > 0) {
+ if (a == 0 && (result == 0xffffff || this.getEmulationMode() < 2.16)) {
+ this.setToolByType(Neo.eraserTip.tools[Neo.eraserTip.mode]);
+
+ } else {
+ if (Neo.eraserTip.selected) {
+ this.setToolByType(Neo.penTip.tools[Neo.penTip.mode]);
+ }
+ }
+ }
+};
+
+Neo.Painter.prototype.fillHorizontalLine = function(buf32, x0, x1, y) {
+ var index = y * this.canvasWidth + x0;
+ var fillColor = this.getColor();
+ for (var x = x0; x <= x1; x++) {
+ buf32[index++] = fillColor;
+ }
+};
+
+Neo.Painter.prototype.scanLine = function(x0, x1, y, baseColor, buf32, stack) {
+ var width = this.canvasWidth;
+
+ while (x0 <= x1) {
+ for (; x0 <= x1; x0++) {
+ if (buf32[y * width + x0] == baseColor) break;
+ }
+ if (x1 < x0) break;
+
+ for (; x0 <= x1; x0++) {
+ if (buf32[y * width + x0] != baseColor) break;
+ }
+ stack.push({x:x0 - 1, y: y})
+ }
+};
+
+Neo.Painter.prototype.fill = function(x, y, ctx) {
+ // http://sandbox.serendip.ws/javascript_canvas_scanline_seedfill.html
+ x = Math.round(x);
+ y = Math.round(y);
+
+ var imageData = ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var width = imageData.width;
+ var stack = [{x: x, y: y}];
+
+ var baseColor = buf32[y * width + x];
+ var fillColor = this.getColor();
+
+ if ((baseColor & 0xffffff00) == 0 ||
+ (baseColor & 0xffffff) != (fillColor & 0xffffff)) {
+ while (stack.length > 0) {
+ var point = stack.pop();
+ var x0 = point.x;
+ var x1 = point.x;
+ var y = point.y;
+
+ if (buf32[y * width + x] == fillColor)
+ break;
+
+ for (; 0 < x0; x0--) {
+ if (buf32[y * width + (x0 - 1)] != baseColor) break;
+ }
+ for (; x1 < this.canvasHeight - 1; x1++) {
+ if (buf32[y * width + (x1 + 1)] != baseColor) break;
+ }
+ this.fillHorizontalLine(buf32, x0, x1, y);
+
+ if (y + 1 < this.canvasHeight) {
+ this.scanLine(x0, x1, y + 1, baseColor, buf32, stack);
+ }
+ if (y - 1 >= 0) {
+ this.scanLine(x0, x1, y - 1, baseColor, buf32, stack);
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.copy = function(x, y, width, height) {
+ this.tempX = 0;
+ this.tempY = 0;
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+
+ var imageData = this.canvasCtx[this.current].getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ this.temp = new Uint32Array(buf32.length);
+ for (var i = 0; i < buf32.length; i++) {
+ this.temp[i] = buf32[i];
+ }
+
+ //tempCanvasã«ä¹—ã›ã‚‹ç”»åƒã‚’作る
+ imageData = this.tempCanvasCtx.getImageData(x, y, width, height);
+ buf32 = new Uint32Array(imageData.data.buffer);
+ buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ for (var i = 0; i < buf32.length; i++) {
+ if (this.temp[i] >> 24) {
+ buf32[i] = this.temp[i] | 0xff000000;
+ } else {
+ buf32[i] = 0xffffffff;
+ }
+ }
+ imageData.data.set(buf8);
+ this.tempCanvasCtx.putImageData(imageData, x, y);
+};
+
+
+Neo.Painter.prototype.paste = function(x, y, width, height) {
+ var ctx = this.canvasCtx[this.current];
+// console.log(this.tempX, this.tempY);
+
+ var imageData = ctx.getImageData(x + this.tempX, y + this.tempY, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ for (var i = 0; i < buf32.length; i++) {
+ buf32[i] = this.temp[i];
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x + this.tempX, y + this.tempY);
+
+ this.temp = null;
+ this.tempX = 0;
+ this.tempY = 0;
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.turn = function(x, y, width, height) {
+ var ctx = this.canvasCtx[this.current];
+
+ // 傾ã‘ツールã®ãƒã‚°ã‚’å†ç¾ã™ã‚‹ãŸã‚一番上ã®ãƒ©ã‚¤ãƒ³ã§å¯¾è±¡é ˜åŸŸã‚’埋ã‚ã‚‹
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var temp = new Uint32Array(buf32.length);
+
+ var index = 0;
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ temp[index] = buf32[index];
+ if (index >= width) {
+ buf32[index] = buf32[index % width];
+ }
+ index++;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+
+ // 90度回転ã•ã›ã¦è²¼ã‚Šä»˜ã‘
+ imageData = ctx.getImageData(x, y, height, width);
+ buf32 = new Uint32Array(imageData.data.buffer);
+ buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ index = 0;
+ for (var j = height - 1; j >= 0; j--) {
+ for (var i = 0; i < width; i++) {
+ buf32[i * height + j] = temp[index++];
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.doFill = function(ctx, x, y, width, height, maskFunc) {
+ if (Math.round(x) != x) console.log("*");
+ if (Math.round(width) != width) console.log("*");
+ if (Math.round(height) != height) console.log("*");
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var index = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.ALPHATYPE_FILL);
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ if (maskFunc && maskFunc.call(this, i, j, width, height)) {
+ //ãªãœã‹åŠ ç®—逆加算ã¯é©ç”¨ã•ã‚Œãªã„
+ if (this.maskType >= Neo.Painter.MASKTYPE_ADD ||
+ !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+
+ if (a > 0) {
+ var a1x = a1;
+ var ax = 1 + a0 * (1 - a1x);
+
+ var r = (r1 + r0 * a0 * (1 - a1x)) / ax;
+ var g = (g1 + g0 * a0 * (1 - a1x)) / ax;
+ var b = (b1 + b0 * a0 * (1 - a1x)) / ax
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+ }
+ }
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.rectFillMask = function(x, y, width, height) {
+ return true;
+};
+
+Neo.Painter.prototype.rectMask = function(x, y, width, height) {
+ var d = this.lineWidth;
+ return (x < d || x > width - 1 - d ||
+ y < d || y > height - 1 - d) ? true : false;
+};
+
+Neo.Painter.prototype.ellipseFillMask = function(x, y, width, height) {
+ var cx = (width - 1) / 2.0;
+ var cy = (height - 1) / 2.0;
+ x = (x - cx) / (cx + 1);
+ y = (y - cy) / (cy + 1);
+
+ return ((x * x) + (y * y) < 1) ? true : false;
+}
+
+Neo.Painter.prototype.ellipseMask = function(x, y, width, height) {
+ var d = this.lineWidth;
+ var cx = (width - 1) / 2.0;
+ var cy = (height - 1) / 2.0;
+
+ if (cx <= d || cy <= d) return this.ellipseFillMask(x, y, width, height);
+
+ var x2 = (x - cx) / (cx - d + 1);
+ var y2 = (y - cy) / (cy - d + 1);
+
+ x = (x - cx) / (cx + 1);
+ y = (y - cy) / (cy + 1);
+
+ if ((x * x) + (y * y) < 1) {
+ if ((x2 * x2) + (y2 * y2) >= 1) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+-----------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.getDestCanvasPosition = function(mx, my, isClip, isCenter) {
+ var mx = Math.floor(mx); //Math.round(mx);
+ var my = Math.floor(my); //Math.round(my);
+ if (isCenter) {
+ mx += 0.499;
+ my += 0.499;
+ }
+ var x = (mx - this.zoomX + this.destCanvas.width * 0.5 / this.zoom) * this.zoom;
+ var y = (my - this.zoomY + this.destCanvas.height * 0.5 / this.zoom) * this.zoom;
+
+ if (isClip) {
+ x = Math.max(Math.min(x, this.destCanvas.width), 0);
+ y = Math.max(Math.min(y, this.destCanvas.height), 0);
+ }
+ return {x:x, y:y};
+};
+
+Neo.Painter.prototype.isWidget = function(element) {
+ while (1) {
+ if (element == null ||
+ element.id == "canvas" ||
+ element.id == "container") break;
+
+ if (element.id == "tools" ||
+ element.className == "buttonOn" ||
+ element.className == "buttonOff" ||
+ element.className == "inputText") {
+ return true;
+ }
+ element = element.parentNode;
+ }
+ return false;
+};
+
+
+Neo.Painter.prototype.loadImage = function (filename) {
+ console.log("loadImage " + filename);
+ var img = new Image();
+ img.src = filename;
+ img.onload = function() {
+ var oe = Neo.painter;
+ oe.canvasCtx[0].drawImage(img, 0, 0);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight);
+ };
+}
+
+Neo.Painter.prototype.loadSession = function (filename) {
+ if (sessionStorage) {
+ var img0 = new Image();
+ img0.src = sessionStorage.getItem('layer0');
+ img0.onload = function() {
+ var img1 = new Image();
+ img1.src = sessionStorage.getItem('layer1');
+ img1.onload = function() {
+ var oe = Neo.painter;
+ oe.canvasCtx[0].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.canvasCtx[1].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.canvasCtx[0].drawImage(img0, 0, 0);
+ oe.canvasCtx[1].drawImage(img1, 0, 0);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight);
+ }
+ }
+ }
+};
+
+Neo.Painter.prototype.saveSession = function() {
+ if (sessionStorage) {
+ sessionStorage.setItem('timestamp', +(new Date()));
+ sessionStorage.setItem('layer0', this.canvas[0].toDataURL('image/png'));
+ sessionStorage.setItem('layer1', this.canvas[1].toDataURL('image/png'));
+ }
+};
+
+Neo.Painter.prototype.clearSession = function() {
+ if (sessionStorage) {
+ sessionStorage.removeItem('timestamp');
+ sessionStorage.removeItem('layer0');
+ sessionStorage.removeItem('layer1');
+ }
+};
+
+Neo.Painter.prototype.sortColor = function(r0, g0, b0) {
+ var min = (r0 < g0) ? ((r0 < b0) ? 0 : 2) : ((g0 < b0) ? 1 : 2);
+ var max = (r0 > g0) ? ((r0 > b0) ? 0 : 2) : ((g0 > b0) ? 1 : 2);
+ var mid = (min + max == 1) ? 2 : ((min + max == 2) ? 1 : 0);
+ return [min, mid, max];
+};
+
+Neo.Painter.prototype.doText = function(x, y, string, fontSize) {
+ //テキストæç”»
+ //æç”»ä½ç½®ãŒãšã‚Œã‚‹ã®ã§é©å½“ã«èª¿æ•´
+ var offset = parseInt(fontSize, 10);
+// y -= Math.round((5.0 + offset/8) / this.zoom);
+// x += Math.round(2.0 / this.zoom);
+
+ var ctx = this.tempCanvasCtx;
+ ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ ctx.save();
+ ctx.translate(x, y);
+// ctx.scale(1/this.zoom, 1/this.zoom);
+
+ ctx.font = fontSize + " Arial";
+ ctx.fillStyle = 0;
+ ctx.fillText(string, 0, 0);
+ ctx.restore();
+
+ // é©å½“ã«äºŒå€¤åŒ–
+ var c = this.getColor();
+ var r = c & 0xff;
+ var g = (c & 0xff00) >> 8;
+ var b = (c & 0xff0000) >> 16;
+ var a = Math.round(this.alpha * 255.0);
+
+ var imageData = ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var length = this.canvasWidth * this.canvasHeight;
+ var index = 0;
+ for (var i = 0; i < length; i++) {
+ if (buf8[index + 3] >= 0x60) {
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ } else {
+ buf8[index + 0] = 0;
+ buf8[index + 1] = 0;
+ buf8[index + 2] = 0;
+ buf8[index + 3] = 0;
+ }
+ index += 4;
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+
+ //キャンãƒã‚¹ã«è²¼ã‚Šä»˜ã‘
+ ctx = this.canvasCtx[this.current];
+ ctx.globalAlpha = 1.0;
+ ctx.drawImage(this.tempCanvas,
+ 0, 0, this.canvasWidth, this.canvasHeight,
+ 0, 0, this.canvasWidth, this.canvasHeight);
+
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.isUIPaused = function() {
+ if (this.drawType == Neo.Painter.DRAWTYPE_BEZIER) {
+ if (this.tool.step && this.tool.step > 0) {
+ return true;
+ }
+ }
+ return false;
+};
+
+Neo.Painter.prototype.getEmulationMode = function() {
+ return parseFloat(Neo.config.neo_emulation_mode || 2.22)
+};
+
+'use strict';
+
+Neo.ToolBase = function() {};
+
+Neo.ToolBase.prototype.startX;
+Neo.ToolBase.prototype.startY;
+Neo.ToolBase.prototype.init = function(oe) {}
+Neo.ToolBase.prototype.kill = function(oe) {}
+Neo.ToolBase.prototype.lineType = Neo.Painter.LINETYPE_NONE;
+
+Neo.ToolBase.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+};
+
+Neo.ToolBase.prototype.upHandler = function(oe) {
+};
+
+Neo.ToolBase.prototype.moveHandler = function(oe) {
+};
+
+Neo.ToolBase.prototype.transformForZoom = function(oe) {
+ var ctx = oe.destCanvasCtx;
+ ctx.translate(oe.canvasWidth * 0.5, oe.canvasHeight * 0.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+};
+
+Neo.ToolBase.prototype.getType = function() {
+ return this.type;
+};
+
+Neo.ToolBase.prototype.getToolButton = function() {
+ switch (this.type) {
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TEXT:
+ return Neo.penTip;
+
+ case Neo.Painter.TOOLTYPE_TONE:
+ case Neo.Painter.TOOLTYPE_BLUR:
+ case Neo.Painter.TOOLTYPE_DODGE:
+ case Neo.Painter.TOOLTYPE_BURN:
+ return Neo.pen2Tip;
+
+ case Neo.Painter.TOOLTYPE_RECT:
+ case Neo.Painter.TOOLTYPE_RECTFILL:
+ case Neo.Painter.TOOLTYPE_ELLIPSE:
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:
+ return Neo.effectTip;
+
+ case Neo.Painter.TOOLTYPE_COPY:
+ case Neo.Painter.TOOLTYPE_MERGE:
+ case Neo.Painter.TOOLTYPE_BLURRECT:
+ case Neo.Painter.TOOLTYPE_FLIP_H:
+ case Neo.Painter.TOOLTYPE_FLIP_V:
+ case Neo.Painter.TOOLTYPE_TURN:
+ return Neo.effect2Tip;
+
+ case Neo.Painter.TOOLTYPE_ERASER:
+ case Neo.Painter.TOOLTYPE_ERASEALL:
+ case Neo.Painter.TOOLTYPE_ERASERECT:
+ return Neo.eraserTip;
+
+ case Neo.Painter.TOOLTYPE_FILL:
+ return Neo.fillButton;
+ }
+ return null;
+};
+
+Neo.ToolBase.prototype.getReserve = function() {
+ switch (this.type) {
+ case Neo.Painter.TOOLTYPE_ERASER:
+ return Neo.reserveEraser;
+
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TONE:
+ case Neo.Painter.TOOLTYPE_ERASERECT:
+ case Neo.Painter.TOOLTYPE_ERASEALL:
+ case Neo.Painter.TOOLTYPE_COPY:
+ case Neo.Painter.TOOLTYPE_MERGE:
+ case Neo.Painter.TOOLTYPE_FIP_H:
+ case Neo.Painter.TOOLTYPE_FIP_V:
+
+ case Neo.Painter.TOOLTYPE_DODGE:
+ case Neo.Painter.TOOLTYPE_BURN:
+ case Neo.Painter.TOOLTYPE_BLUR:
+ case Neo.Painter.TOOLTYPE_BLURRECT:
+
+ case Neo.Painter.TOOLTYPE_TEXT:
+ case Neo.Painter.TOOLTYPE_TURN:
+ case Neo.Painter.TOOLTYPE_RECT:
+ case Neo.Painter.TOOLTYPE_RECTFILL:
+ case Neo.Painter.TOOLTYPE_ELLIPSE:
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:
+ return Neo.reservePen;
+
+ }
+ return null;
+};
+
+Neo.ToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.updateUI();
+ }
+};
+
+Neo.ToolBase.prototype.saveStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ reserve.size = Neo.painter.lineWidth;
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ DrawToolBase(æ画ツールã®ãƒ™ãƒ¼ã‚¹ã‚¯ãƒ©ã‚¹ï¼‰
+-------------------------------------------------------------------------
+*/
+
+Neo.DrawToolBase = function() {};
+Neo.DrawToolBase.prototype = new Neo.ToolBase();
+Neo.DrawToolBase.prototype.isUpMove = false;
+Neo.DrawToolBase.prototype.step = 0;
+
+Neo.DrawToolBase.prototype.init = function() {
+ this.step = 0;
+ this.isUpMove = true;
+};
+
+Neo.DrawToolBase.prototype.downHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandDownHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineDownHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierDownHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.upHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandUpHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineUpHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierUpHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.moveHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierMoveHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.upMoveHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandUpMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineUpMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierUpMoveHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.keyDownHandler = function(e) {
+ switch (Neo.painter.drawType) {
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierKeyDownHandler(e); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.rollOverHandler= function(oe) {};
+Neo.DrawToolBase.prototype.rollOutHandler= function(oe) {
+ if (!oe.isMouseDown && !oe.isMouseDownRight){
+ oe.tempCanvasCtx.clearRect(0,0,oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.DrawToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+};
+
+
+/* FreeHand (手書ã) */
+
+Neo.DrawToolBase.prototype.freeHandDownHandler = function(oe) {
+ //Register undo first;
+ oe._pushUndo();
+
+ oe.prepareDrawing();
+ this.isUpMove = false;
+ var ctx = oe.canvasCtx[oe.current];
+ if (oe.alpha >= 1 || this.lineType != Neo.Painter.LINETYPE_BRUSH) {
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ oe.drawLine(ctx, x0, y0, x0, y0, this.lineType);
+ }
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+ if (oe.alpha >= 1) {
+ var r = Math.ceil(oe.lineWidth / 2);
+ var rect = oe.getBound(oe.mouseX, oe.mouseY, oe.mouseX, oe.mouseY, r);
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ }
+};
+
+Neo.DrawToolBase.prototype.freeHandUpHandler = function(oe) {
+ oe.tempCanvasCtx.clearRect(0,0,oe.canvasWidth, oe.canvasHeight);
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+// oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+// this.drawCursor(oe);
+ oe.prevLine = null;
+};
+
+Neo.DrawToolBase.prototype.freeHandMoveHandler = function(oe) {
+ var ctx = oe.canvasCtx[oe.current];
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ var x1 = Math.floor(oe.prevMouseX);
+ var y1 = Math.floor(oe.prevMouseY);
+ oe.drawLine(ctx, x0, y0, x1, y1, this.lineType);
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+ var r = Math.ceil(oe.lineWidth / 2);
+ var rect = oe.getBound(oe.mouseX, oe.mouseY, oe.prevMouseX, oe.prevMouseY, r);
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+};
+
+Neo.DrawToolBase.prototype.freeHandUpMoveHandler = function(oe) {
+ this.isUpMove = true;
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+ this.drawCursor(oe);
+};
+
+Neo.DrawToolBase.prototype.drawCursor = function(oe) {
+ if (oe.lineWidth <= 8) return;
+ var mx = oe.mouseX;
+ var my = oe.mouseY;
+ var d = oe.lineWidth;
+
+ var x = (mx - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y = (my - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ var r = d * 0.5 * oe.zoom;
+
+ if (!(x > -r &&
+ y > -r &&
+ x < oe.destCanvas.width + r &&
+ y < oe.destCanvas.height + r)) return;
+
+ var ctx = oe.destCanvasCtx;
+ ctx.save();
+ this.transformForZoom(oe)
+
+ var c = (this.type == Neo.Painter.TOOLTYPE_ERASER) ? 0x0000ff : 0xffff7f;
+ oe.drawXOREllipse(ctx, x-r, y-r, r*2, r*2, false, c);
+
+ ctx.restore();
+ oe.cursorRect = oe.getBound(mx, my, mx, my, Math.ceil(d / 2));
+}
+
+
+/* Line (ç›´ç·š) */
+
+Neo.DrawToolBase.prototype.lineDownHandler = function(oe) {
+ this.isUpMove = false;
+ this.startX = Math.floor(oe.mouseX);
+ this.startY = Math.floor(oe.mouseY);
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+};
+
+Neo.DrawToolBase.prototype.lineUpHandler = function(oe) {
+ if (this.isUpMove == false) {
+ this.isUpMove = true;
+
+ oe._pushUndo();
+ oe.prepareDrawing();
+ var ctx = oe.canvasCtx[oe.current];
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ oe.drawLine(ctx, x0, y0, this.startX, this.startY, this.lineType);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.DrawToolBase.prototype.lineMoveHandler = function(oe) {
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ this.drawLineCursor(oe);
+};
+
+Neo.DrawToolBase.prototype.lineUpMoveHandler = function(oe) {
+};
+
+Neo.DrawToolBase.prototype.drawLineCursor = function(oe, mx, my) {
+ if (!mx) mx = Math.floor(oe.mouseX);
+ if (!my) my = Math.floor(oe.mouseY);
+ var nx = this.startX;
+ var ny = this.startY;
+ var ctx = oe.destCanvasCtx;
+ ctx.save();
+ this.transformForZoom(oe)
+
+ var x0 = (mx +.499 - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y0 = (my +.499 - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ var x1 = (nx +.499 - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y1 = (ny +.499 - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ oe.drawXORLine(ctx, x0, y0, x1, y1);
+
+ ctx.restore();
+};
+
+
+/* Bezier (BZ曲線) */
+
+Neo.DrawToolBase.prototype.bezierDownHandler = function(oe) {
+ this.isUpMove = false;
+
+ if (this.step == 0) {
+ this.startX = this.x0 = Math.floor(oe.mouseX);
+ this.startY = this.y0 = Math.floor(oe.mouseY);
+ }
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+};
+
+Neo.DrawToolBase.prototype.bezierUpHandler = function(oe) {
+ if (this.isUpMove == false) {
+ this.isUpMove = true;
+ }
+
+ this.step++;
+ switch (this.step) {
+ case 1:
+ oe.prepareDrawing();
+ this.x3 = Math.floor(oe.mouseX);
+ this.y3 = Math.floor(oe.mouseY);
+ break;
+
+ case 2:
+ this.x1 = Math.floor(oe.mouseX);
+ this.y1 = Math.floor(oe.mouseY);
+ break;
+
+ case 3:
+ this.x2 = Math.floor(oe.mouseX);
+ this.y2 = Math.floor(oe.mouseY);
+
+ oe._pushUndo();
+ oe.drawBezier(oe.canvasCtx[oe.current],
+ this.x0, this.y0, this.x1, this.y1,
+ this.x2, this.y2, this.x3, this.y3, this.lineType);
+
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ this.step = 0;
+ break;
+
+ default:
+ this.step = 0;
+ break;
+ }
+};
+
+Neo.DrawToolBase.prototype.bezierMoveHandler = function(oe) {
+ switch (this.step) {
+ case 0:
+ if (!this.isUpMove) {
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawLineCursor(oe);
+ }
+ break;
+ case 1:
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawBezierCursor1(oe);
+ break;
+
+ case 2:
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawBezierCursor2(oe);
+ break;
+ }
+};
+
+Neo.DrawToolBase.prototype.bezierUpMoveHandler = function(oe) {
+ this.bezierMoveHandler(oe);
+};
+
+Neo.DrawToolBase.prototype.bezierKeyDownHandler = function(e) {
+ if (e.keyCode == 27) { //Escã§ã‚­ãƒ£ãƒ³ã‚»ãƒ«
+ this.step = 0;
+
+ var oe = Neo.painter;
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+
+Neo.DrawToolBase.prototype.drawBezierCursor1 = function(oe) {
+ var ctx = oe.destCanvasCtx;
+// var x = oe.mouseX; //Math.floor(oe.mouseX);
+// var y = oe.mouseY; //Math.floor(oe.mouseY);
+ var stab = oe.getStabilized();
+ var x = Math.floor(stab[0]);
+ var y = Math.floor(stab[1]);
+ var p = oe.getDestCanvasPosition(x, y, false, true);
+ var p0 = oe.getDestCanvasPosition(this.x0, this.y0, false, true);
+ var p3 = oe.getDestCanvasPosition(this.x3, this.y3, false, true);
+
+ // handle
+ oe.drawXORLine(ctx, p0.x, p0.y, p.x, p.y);
+ oe.drawXOREllipse(ctx, p.x - 4, p.y - 4, 8, 8);
+ oe.drawXOREllipse(ctx, p0.x - 4, p0.y - 4, 8, 8);
+
+ // preview
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.drawBezier(oe.tempCanvasCtx,
+ this.x0, this.y0,
+ x, y,
+ x, y,
+ this.x3, this.y3, this.lineType);
+
+ ctx.save();
+ ctx.translate(oe.destCanvas.width*.5, oe.destCanvas.height*.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+ ctx.drawImage(oe.tempCanvas,
+ 0, 0, oe.canvasWidth, oe.canvasHeight,
+ 0, 0, oe.canvasWidth, oe.canvasHeight);
+
+ ctx.restore();
+};
+
+Neo.DrawToolBase.prototype.drawBezierCursor2 = function(oe) {
+ var ctx = oe.destCanvasCtx;
+// var x = oe.mouseX; //Math.floor(oe.mouseX);
+// var y = oe.mouseY; //Math.floor(oe.mouseY);
+ var stab = oe.getStabilized();
+ var x = Math.floor(stab[0]);
+ var y = Math.floor(stab[1]);
+ var p = oe.getDestCanvasPosition(oe.mouseX, oe.mouseY, false, true);
+ var p0 = oe.getDestCanvasPosition(this.x0, this.y0, false, true);
+ var p1 = oe.getDestCanvasPosition(this.x1, this.y1, false, true);
+ var p3 = oe.getDestCanvasPosition(this.x3, this.y3, false, true);
+
+ // handle
+ oe.drawXORLine(ctx, p3.x, p3.y, p.x, p.y);
+ oe.drawXOREllipse(ctx, p.x - 4, p.y - 4, 8, 8);
+ oe.drawXORLine(ctx, p0.x, p0.y, p1.x, p1.y);
+ oe.drawXOREllipse(ctx, p1.x - 4, p1.y - 4, 8, 8);
+ oe.drawXOREllipse(ctx, p0.x - 4, p0.y - 4, 8, 8);
+
+ // preview
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.drawBezier(oe.tempCanvasCtx,
+ this.x0, this.y0,
+ this.x1, this.y1,
+ x, y,
+ this.x3, this.y3, this.lineType);
+
+ ctx.save();
+ ctx.translate(oe.destCanvas.width*.5, oe.destCanvas.height*.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+ ctx.drawImage(oe.tempCanvas,
+ 0, 0, oe.canvasWidth, oe.canvasHeight,
+ 0, 0, oe.canvasWidth, oe.canvasHeight);
+ ctx.restore();
+};
+
+/*
+-------------------------------------------------------------------------
+ Pen(鉛筆)
+-------------------------------------------------------------------------
+*/
+
+Neo.PenTool = function() {};
+Neo.PenTool.prototype = new Neo.DrawToolBase();
+Neo.PenTool.prototype.type = Neo.Painter.TOOLTYPE_PEN;
+Neo.PenTool.prototype.lineType = Neo.Painter.LINETYPE_PEN;
+
+Neo.PenTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+}
+
+/*
+-------------------------------------------------------------------------
+ Brush(水彩)
+-------------------------------------------------------------------------
+*/
+
+Neo.BrushTool = function() {};
+Neo.BrushTool.prototype = new Neo.DrawToolBase();
+Neo.BrushTool.prototype.type = Neo.Painter.TOOLTYPE_BRUSH;
+Neo.BrushTool.prototype.lineType = Neo.Painter.LINETYPE_BRUSH;
+
+Neo.BrushTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = this.getAlpha();
+ Neo.updateUI();
+ }
+};
+
+Neo.BrushTool.prototype.getAlpha = function() {
+ var alpha = 241 - Math.floor(Neo.painter.lineWidth / 2) * 6;
+ return alpha / 255.0;
+};
+
+/*
+-------------------------------------------------------------------------
+ Tone(トーン)
+-------------------------------------------------------------------------
+*/
+
+Neo.ToneTool = function() {};
+Neo.ToneTool.prototype = new Neo.DrawToolBase();
+Neo.ToneTool.prototype.type = Neo.Painter.TOOLTYPE_TONE;
+Neo.ToneTool.prototype.lineType = Neo.Painter.LINETYPE_TONE;
+
+Neo.ToneTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 23 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Eraser(消ã—ペン)
+-------------------------------------------------------------------------
+*/
+
+Neo.EraserTool = function() {};
+Neo.EraserTool.prototype = new Neo.DrawToolBase();
+Neo.EraserTool.prototype.type = Neo.Painter.TOOLTYPE_ERASER;
+Neo.EraserTool.prototype.lineType = Neo.Painter.LINETYPE_ERASER;
+
+
+/*
+-------------------------------------------------------------------------
+ Blur(ã¼ã‹ã—)
+-------------------------------------------------------------------------
+*/
+
+Neo.BlurTool = function() {};
+Neo.BlurTool.prototype = new Neo.DrawToolBase();
+Neo.BlurTool.prototype.type = Neo.Painter.TOOLTYPE_BLUR;
+Neo.BlurTool.prototype.lineType = Neo.Painter.LINETYPE_BLUR;
+
+Neo.BlurTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Dodge(覆ã„焼ã)
+-------------------------------------------------------------------------
+*/
+
+Neo.DodgeTool = function() {};
+Neo.DodgeTool.prototype = new Neo.DrawToolBase();
+Neo.DodgeTool.prototype.type = Neo.Painter.TOOLTYPE_DODGE;
+Neo.DodgeTool.prototype.lineType = Neo.Painter.LINETYPE_DODGE;
+
+Neo.DodgeTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Burn(焼ãè¾¼ã¿ï¼‰
+-------------------------------------------------------------------------
+*/
+
+Neo.BurnTool = function() {};
+Neo.BurnTool.prototype = new Neo.DrawToolBase();
+Neo.BurnTool.prototype.type = Neo.Painter.TOOLTYPE_BURN;
+Neo.BurnTool.prototype.lineType = Neo.Painter.LINETYPE_BURN;
+
+Neo.BurnTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Hand(スクロール)
+-------------------------------------------------------------------------
+*/
+
+Neo.HandTool = function() {};
+Neo.HandTool.prototype = new Neo.ToolBase();
+Neo.HandTool.prototype.type = Neo.Painter.TOOLTYPE_HAND;
+Neo.HandTool.prototype.isUpMove = false;
+Neo.HandTool.prototype.reverse = false;
+
+Neo.HandTool.prototype.downHandler = function(oe) {
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+
+ this.isDrag = true;
+ this.startX = oe.rawMouseX;
+ this.startY = oe.rawMouseY;
+};
+
+Neo.HandTool.prototype.upHandler = function(oe) {
+ this.isDrag = false;
+ oe.popTool();
+};
+
+Neo.HandTool.prototype.moveHandler = function(oe) {
+ if (this.isDrag) {
+ var dx = this.startX - oe.rawMouseX;
+ var dy = this.startY - oe.rawMouseY;
+
+ var ax = oe.destCanvas.width / (oe.canvasWidth * oe.zoom);
+ var ay = oe.destCanvas.height / (oe.canvasHeight * oe.zoom);
+ var barWidth = oe.destCanvas.width * ax;
+ var barHeight = oe.destCanvas.height * ay;
+ var scrollWidthInScreen = oe.destCanvas.width - barWidth - 2;
+ var scrollHeightInScreen = oe.destCanvas.height - barHeight - 2;
+
+ dx *= oe.scrollWidth / scrollWidthInScreen;
+ dy *= oe.scrollHeight / scrollHeightInScreen;
+
+ if (this.reverse) {
+ dx *= -1;
+ dy *= -1;
+ }
+
+ oe.setZoomPosition(oe.zoomX - dx, oe.zoomY - dy);
+
+ this.startX = oe.rawMouseX;
+ this.startY = oe.rawMouseY;
+ }
+};
+
+Neo.HandTool.prototype.rollOutHandler= function(oe) {};
+Neo.HandTool.prototype.upMoveHandler = function(oe) {}
+Neo.HandTool.prototype.rollOverHandler= function(oe) {}
+
+/*
+-------------------------------------------------------------------------
+ Slider(色やサイズã®ã‚¹ãƒ©ã‚¤ãƒ€ã‚’æ“作ã—ã¦ã„る時)
+-------------------------------------------------------------------------
+*/
+
+Neo.SliderTool = function() {};
+Neo.SliderTool.prototype = new Neo.ToolBase();
+Neo.SliderTool.prototype.type = Neo.Painter.TOOLTYPE_SLIDER;
+Neo.SliderTool.prototype.isUpMove = false;
+Neo.SliderTool.prototype.alt = false;
+
+Neo.SliderTool.prototype.downHandler = function(oe) {
+ if (!oe.isShiftDown) this.isDrag = true;
+
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].downHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+};
+
+Neo.SliderTool.prototype.upHandler = function(oe) {
+ this.isDrag = false;
+ oe.popTool();
+
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].upHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+};
+
+Neo.SliderTool.prototype.moveHandler = function(oe) {
+ if (this.isDrag) {
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].moveHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+ }
+};
+
+Neo.SliderTool.prototype.upMoveHandler = function(oe) {}
+Neo.SliderTool.prototype.rollOutHandler= function(oe) {};
+Neo.SliderTool.prototype.rollOverHandler= function(oe) {}
+
+/*
+-------------------------------------------------------------------------
+ Fill(塗り潰ã—)
+-------------------------------------------------------------------------
+*/
+
+Neo.FillTool = function() {};
+Neo.FillTool.prototype = new Neo.ToolBase();
+Neo.FillTool.prototype.type = Neo.Painter.TOOLTYPE_FILL;
+Neo.FillTool.prototype.isUpMove = false;
+
+Neo.FillTool.prototype.downHandler = function(oe) {
+ var x = Math.floor(oe.mouseX);
+ var y = Math.floor(oe.mouseY);
+ oe._pushUndo();
+ oe.fill(x, y, oe.canvasCtx[oe.current]);
+};
+
+Neo.FillTool.prototype.upHandler = function(oe) {
+};
+
+Neo.FillTool.prototype.moveHandler = function(oe) {
+};
+
+Neo.FillTool.prototype.rollOutHandler= function(oe) {};
+Neo.FillTool.prototype.upMoveHandler = function(oe) {}
+Neo.FillTool.prototype.rollOverHandler= function(oe) {}
+
+
+/*
+-------------------------------------------------------------------------
+ EraseAll(全消ã—)
+-------------------------------------------------------------------------
+*/
+
+Neo.EraseAllTool = function() {};
+Neo.EraseAllTool.prototype = new Neo.ToolBase();
+Neo.EraseAllTool.prototype.type = Neo.Painter.TOOLTYPE_ERASEALL;
+Neo.EraseAllTool.prototype.isUpMove = false;
+
+Neo.EraseAllTool.prototype.downHandler = function(oe) {
+ oe._pushUndo();
+
+ oe.prepareDrawing();
+ oe.canvasCtx[oe.current].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+Neo.EraseAllTool.prototype.upHandler = function(oe) {
+};
+
+Neo.EraseAllTool.prototype.moveHandler = function(oe) {
+};
+
+Neo.EraseAllTool.prototype.rollOutHandler= function(oe) {};
+Neo.EraseAllTool.prototype.upMoveHandler = function(oe) {};
+Neo.EraseAllTool.prototype.rollOverHandler= function(oe) {};
+
+
+/*
+-------------------------------------------------------------------------
+ EffectToolBase(エフェックトツールã®ãƒ™ãƒ¼ã‚¹ã‚¯ãƒ©ã‚¹ï¼‰
+-------------------------------------------------------------------------
+*/
+
+Neo.EffectToolBase = function() {};
+Neo.EffectToolBase.prototype = new Neo.ToolBase();
+Neo.EffectToolBase.prototype.isUpMove = false;
+
+Neo.EffectToolBase.prototype.downHandler = function(oe) {
+ this.isUpMove = false;
+
+ this.startX = this.endX = oe.clipMouseX;
+ this.startY = this.endY = oe.clipMouseY;
+};
+
+Neo.EffectToolBase.prototype.upHandler = function(oe) {
+ if (this.isUpMove) return;
+ this.isUpMove = true;
+
+ this.startX = Math.floor(this.startX);
+ this.startY = Math.floor(this.startY);
+ this.endX = Math.floor(this.endX);
+ this.endY = Math.floor(this.endY);
+
+ var x = (this.startX < this.endX) ? this.startX : this.endX;
+ var y = (this.startY < this.endY) ? this.startY : this.endY;
+ var width = Math.abs(this.startX - this.endX) + 1;
+ var height = Math.abs(this.startY - this.endY) + 1;
+ var ctx = oe.canvasCtx[oe.current];
+
+ if (x < 0) x = 0;
+ if (y < 0) y = 0;
+ if (x + width > oe.canvasWidth) width = oe.canvasWidth - x;
+ if (y + height > oe.canvasHeight) height = oe.canvasHeight - y;
+
+ if (width > 0 && height > 0) {
+ oe._pushUndo();
+ oe.prepareDrawing();
+ this.doEffect(oe, x, y, width, height);
+ }
+
+ if (oe.tool.type != Neo.Painter.TOOLTYPE_PASTE) {
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.EffectToolBase.prototype.moveHandler = function(oe) {
+ this.endX = oe.clipMouseX;
+ this.endY = oe.clipMouseY;
+
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ this.drawCursor(oe);
+};
+
+Neo.EffectToolBase.prototype.rollOutHandler= function(oe) {};
+Neo.EffectToolBase.prototype.upMoveHandler = function(oe) {};
+Neo.EffectToolBase.prototype.rollOverHandler= function(oe) {};
+
+Neo.EffectToolBase.prototype.drawCursor = function(oe) {
+ var ctx = oe.destCanvasCtx;
+
+ ctx.save();
+ this.transformForZoom(oe);
+
+ var start = oe.getDestCanvasPosition(this.startX, this.startY, true);
+ var end = oe.getDestCanvasPosition(this.endX, this.endY, true);
+
+ var x = (start.x < end.x) ? start.x : end.x;
+ var y = (start.y < end.y) ? start.y : end.y;
+ var width = Math.abs(start.x - end.x) + oe.zoom;
+ var height = Math.abs(start.y - end.y) + oe.zoom;
+
+ if (this.isEllipse) {
+ oe.drawXOREllipse(ctx, x, y, width, height, this.isFill);
+
+ } else {
+ oe.drawXORRect(ctx, x, y, width, height, this.isFill);
+ }
+ ctx.restore();
+};
+
+Neo.EffectToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = this.defaultAlpha || 1.0;
+ Neo.updateUI();
+ };
+};
+
+/*
+-------------------------------------------------------------------------
+ EraseRect(消ã—四角)
+-------------------------------------------------------------------------
+*/
+
+Neo.EraseRectTool = function() {};
+Neo.EraseRectTool.prototype = new Neo.EffectToolBase();
+Neo.EraseRectTool.prototype.type = Neo.Painter.TOOLTYPE_ERASERECT;
+
+Neo.EraseRectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.eraseRect(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ FlipH(左å³å転)
+-------------------------------------------------------------------------
+*/
+
+Neo.FlipHTool = function() {};
+Neo.FlipHTool.prototype = new Neo.EffectToolBase();
+Neo.FlipHTool.prototype.type = Neo.Painter.TOOLTYPE_FLIP_H;
+
+Neo.FlipHTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.flipH(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ FlipV(上下å転)
+-------------------------------------------------------------------------
+*/
+
+Neo.FlipVTool = function() {};
+Neo.FlipVTool.prototype = new Neo.EffectToolBase();
+Neo.FlipVTool.prototype.type = Neo.Painter.TOOLTYPE_FLIP_V;
+
+Neo.FlipVTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.flipV(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ DodgeRect(角å–り)
+-------------------------------------------------------------------------
+*/
+
+Neo.BlurRectTool = function() {};
+Neo.BlurRectTool.prototype = new Neo.EffectToolBase();
+Neo.BlurRectTool.prototype.type = Neo.Painter.TOOLTYPE_BLURRECT;
+
+Neo.BlurRectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.blurRect(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+Neo.BlurRectTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 0.5;
+ Neo.updateUI();
+ };
+}
+
+/*
+-------------------------------------------------------------------------
+ Turn(傾ã‘)
+-------------------------------------------------------------------------
+*/
+
+Neo.TurnTool = function() {};
+Neo.TurnTool.prototype = new Neo.EffectToolBase();
+Neo.TurnTool.prototype.type = Neo.Painter.TOOLTYPE_TURN;
+
+Neo.TurnTool.prototype.upHandler = function(oe) {
+ this.isUpMove = true;
+
+ this.startX = Math.floor(this.startX);
+ this.startY = Math.floor(this.startY);
+ this.endX = Math.floor(this.endX);
+ this.endY = Math.floor(this.endY);
+
+ var x = (this.startX < this.endX) ? this.startX : this.endX;
+ var y = (this.startY < this.endY) ? this.startY : this.endY;
+ var width = Math.abs(this.startX - this.endX) + 1;
+ var height = Math.abs(this.startY - this.endY) + 1;
+
+ if (width > 0 && height > 0) {
+ oe._pushUndo();
+ oe.turn(x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Merge(レイヤーçµåˆï¼‰
+-------------------------------------------------------------------------
+*/
+
+Neo.MergeTool = function() {};
+Neo.MergeTool.prototype = new Neo.EffectToolBase();
+Neo.MergeTool.prototype.type = Neo.Painter.TOOLTYPE_MERGE;
+
+Neo.MergeTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.merge(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ Copy(コピー)
+-------------------------------------------------------------------------
+*/
+
+Neo.CopyTool = function() {};
+Neo.CopyTool.prototype = new Neo.EffectToolBase();
+Neo.CopyTool.prototype.type = Neo.Painter.TOOLTYPE_COPY;
+
+Neo.CopyTool.prototype.doEffect = function(oe, x, y, width, height) {
+ oe.copy(x, y, width, height);
+ oe.setToolByType(Neo.Painter.TOOLTYPE_PASTE);
+ oe.tool.x = x;
+ oe.tool.y = y;
+ oe.tool.width = width;
+ oe.tool.height = height;
+};
+
+/*
+-------------------------------------------------------------------------
+ Paste(ペースト)
+-------------------------------------------------------------------------
+*/
+
+Neo.PasteTool = function() {};
+Neo.PasteTool.prototype = new Neo.ToolBase();
+Neo.PasteTool.prototype.type = Neo.Painter.TOOLTYPE_PASTE;
+
+Neo.PasteTool.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+ this.drawCursor(oe);
+};
+
+Neo.PasteTool.prototype.upHandler = function(oe) {
+ oe._pushUndo();
+
+ oe.paste(this.x, this.y, this.width, this.height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ oe.setToolByType(Neo.Painter.TOOLTYPE_COPY);
+};
+
+Neo.PasteTool.prototype.moveHandler = function(oe) {
+ var dx = Math.floor(oe.mouseX - this.startX);
+ var dy = Math.floor(oe.mouseY - this.startY);
+ oe.tempX = dx;
+ oe.tempY = dy;
+
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+// this.drawCursor(oe);
+};
+
+Neo.PasteTool.prototype.keyDownHandler = function(e) {
+ if (e.keyCode == 27) { //Escã§ã‚­ãƒ£ãƒ³ã‚»ãƒ«
+ var oe = Neo.painter;
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ oe.setToolByType(Neo.Painter.TOOLTYPE_COPY);
+ }
+};
+
+Neo.PasteTool.prototype.drawCursor = function(oe) {
+ var ctx = oe.destCanvasCtx;
+
+ ctx.save();
+ this.transformForZoom(oe);
+
+ var start = oe.getDestCanvasPosition(this.x, this.y, true);
+ var end = oe.getDestCanvasPosition(this.x + this.width, this.y + this.height, true);
+
+ var x = start.x + oe.tempX * oe.zoom;
+ var y = start.y + oe.tempY * oe.zoom;
+ var width = Math.abs(start.x - end.x);
+ var height = Math.abs(start.y - end.y);
+ oe.drawXORRect(ctx, x, y, width, height);
+ ctx.restore();
+};
+
+/*
+-------------------------------------------------------------------------
+ Rect(線四角)
+-------------------------------------------------------------------------
+*/
+
+Neo.RectTool = function() {};
+Neo.RectTool.prototype = new Neo.EffectToolBase();
+Neo.RectTool.prototype.type = Neo.Painter.TOOLTYPE_RECT;
+
+Neo.RectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.rectMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ RectFill(四角)
+-------------------------------------------------------------------------
+*/
+
+Neo.RectFillTool = function() {};
+Neo.RectFillTool.prototype = new Neo.EffectToolBase();
+Neo.RectFillTool.prototype.type = Neo.Painter.TOOLTYPE_RECTFILL;
+
+Neo.RectFillTool.prototype.isFill = true;
+Neo.RectFillTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.rectFillMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ Ellipse(線楕円)
+-------------------------------------------------------------------------
+*/
+
+Neo.EllipseTool = function() {};
+Neo.EllipseTool.prototype = new Neo.EffectToolBase();
+Neo.EllipseTool.prototype.type = Neo.Painter.TOOLTYPE_ELLIPSE;
+Neo.EllipseTool.prototype.isEllipse = true;
+Neo.EllipseTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.ellipseMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ EllipseFill(楕円)
+-------------------------------------------------------------------------
+*/
+
+Neo.EllipseFillTool = function() {};
+Neo.EllipseFillTool.prototype = new Neo.EffectToolBase();
+Neo.EllipseFillTool.prototype.type = Neo.Painter.TOOLTYPE_ELLIPSEFILL;
+Neo.EllipseFillTool.prototype.isEllipse = true;
+Neo.EllipseFillTool.prototype.isFill = true;
+Neo.EllipseFillTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.ellipseFillMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+-------------------------------------------------------------------------
+ Text(テキスト)
+-------------------------------------------------------------------------
+*/
+
+Neo.TextTool = function() {};
+Neo.TextTool.prototype = new Neo.ToolBase();
+Neo.TextTool.prototype.type = Neo.Painter.TOOLTYPE_TEXT;
+Neo.TextTool.prototype.isUpMove = false;
+
+Neo.TextTool.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+
+ if (Neo.painter.inputText) {
+ Neo.painter.updateInputText();
+
+ var rect = oe.container.getBoundingClientRect();
+ var text = Neo.painter.inputText;
+ var x = oe.rawMouseX - rect.left - 5;
+ var y = oe.rawMouseY - rect.top - 5;
+
+ text.style.left = x + "px";
+ text.style.top = y + "px";
+ text.style.display = "block";
+ text.focus();
+ }
+};
+
+Neo.TextTool.prototype.upHandler = function(oe) {
+};
+
+Neo.TextTool.prototype.moveHandler = function(oe) {};
+Neo.TextTool.prototype.upMoveHandler = function(oe) {};
+Neo.TextTool.prototype.rollOverHandler= function(oe) {};
+Neo.TextTool.prototype.rollOutHandler= function(oe) {};
+
+Neo.TextTool.prototype.keyDownHandler = function(e) {
+ if (e.keyCode == 13) { // Returnã§ç¢ºå®š
+ e.preventDefault();
+
+ var oe = Neo.painter;
+ var text = oe.inputText;
+ if (text) {
+ oe._pushUndo();
+ this.drawText(oe);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ text.style.display = "none";
+ text.blur();
+ }
+ }
+};
+
+Neo.TextTool.prototype.kill = function(oe) {
+ Neo.painter.hideInputText();
+};
+
+Neo.TextTool.prototype.drawText = function(oe) {
+ var text = oe.inputText;
+
+ // unescape entities
+ //var tmp = document.createElement("textarea");
+ //tmp.innerHTML = text.innerHTML;
+ //var string = tmp.value;
+
+ var string = text.textContent || text.innerText;
+
+ if (string.length <= 0) return;
+ oe.doText(this.startX, this.startY, string, text.style.fontSize);
+};
+
+Neo.TextTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+};
+
+/*
+-------------------------------------------------------------------------
+ Dummy(何もã—ãªã„時)
+-------------------------------------------------------------------------
+*/
+
+Neo.DummyTool = function() {};
+Neo.DummyTool.prototype = new Neo.ToolBase();
+Neo.DummyTool.prototype.type = Neo.Painter.TOOLTYPE_NONE;
+Neo.DummyTool.prototype.isUpMove = false;
+
+Neo.DummyTool.prototype.downHandler = function(oe) {
+};
+
+Neo.DummyTool.prototype.upHandler = function(oe) {
+ oe.popTool();
+};
+
+Neo.DummyTool.prototype.moveHandler = function(oe) {};
+Neo.DummyTool.prototype.upMoveHandler = function(oe) {}
+Neo.DummyTool.prototype.rollOverHandler= function(oe) {}
+Neo.DummyTool.prototype.rollOutHandler= function(oe) {}
+
+'use strict';
+
+Neo.CommandBase = function() {
+};
+Neo.CommandBase.prototype.data;
+Neo.CommandBase.prototype.execute = function() {}
+
+
+/*
+---------------------------------------------------
+ ZOOM
+---------------------------------------------------
+*/
+Neo.ZoomPlusCommand = function(data) {this.data = data};
+Neo.ZoomPlusCommand.prototype = new Neo.CommandBase();
+Neo.ZoomPlusCommand.prototype.execute = function() {
+ if (this.data.zoom < 12) {
+ this.data.setZoom(this.data.zoom + 1);
+ }
+ Neo.resizeCanvas();
+ Neo.painter.updateDestCanvas();
+};
+
+Neo.ZoomMinusCommand = function(data) {this.data = data};
+Neo.ZoomMinusCommand.prototype = new Neo.CommandBase();
+Neo.ZoomMinusCommand.prototype.execute = function() {
+ if (this.data.zoom >= 2) {
+ this.data.setZoom(this.data.zoom - 1);
+ }
+ Neo.resizeCanvas();
+ Neo.painter.updateDestCanvas();
+};
+
+/*
+---------------------------------------------------
+ UNDO
+---------------------------------------------------
+*/
+Neo.UndoCommand = function(data) {this.data = data};
+Neo.UndoCommand.prototype = new Neo.CommandBase();
+Neo.UndoCommand.prototype.execute = function() {
+ this.data.undo();
+};
+
+Neo.RedoCommand = function(data) {this.data = data};
+Neo.RedoCommand.prototype = new Neo.CommandBase();
+Neo.RedoCommand.prototype.execute = function() {
+ this.data.redo();
+};
+
+
+/*
+---------------------------------------------------
+---------------------------------------------------
+*/
+
+
+
+Neo.WindowCommand = function(data) {this.data = data};
+Neo.WindowCommand.prototype = new Neo.CommandBase();
+Neo.WindowCommand.prototype.execute = function() {
+ if (Neo.fullScreen) {
+ //if (confirm("¿Vista página?")) {
+ Neo.fullScreen = false;
+ Neo.updateWindow();
+ //}
+ } else {
+ //if (confirm("¿Vista ventana?")) {
+ Neo.fullScreen = true;
+ Neo.updateWindow();
+ //}
+ }
+};
+
+Neo.SubmitCommand = function(data) {this.data = data};
+Neo.SubmitCommand.prototype = new Neo.CommandBase();
+Neo.SubmitCommand.prototype.execute = function() {
+ var board = location.href.replace(/[^/]*$/, '');
+ console.log("submit: " + board);
+ this.data.submit(board);
+};
+
+Neo.CopyrightCommand = function(data) {this.data = data};
+Neo.CopyrightCommand.prototype = new Neo.CommandBase();
+Neo.CopyrightCommand.prototype.execute = function() {
+// var url = "http://hp.vector.co.jp/authors/VA016309/";
+// if (confirm(url + "\nã—ãƒã¡ã‚ƒã‚“ã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ")) {
+ var url = "http://github.com/funige/neo/";
+ if (confirm("PaintBBS NEOã¯ã€ãŠçµµæãã—ãƒæŽ²ç¤ºæ¿ PaintBBS (©2000-2004 ã—ãƒã¡ã‚ƒã‚“) ã‚’html5化ã™ã‚‹ãƒ—ロジェクトã§ã™ã€‚\n\nPaintBBS NEOã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ" + "\n")) {
+ Neo.openURL(url);
+ }
+};
+
+'use strict';
+
+/*
+-------------------------------------------------------------------------
+ Button
+-------------------------------------------------------------------------
+*/
+
+Neo.Button = function() {};
+Neo.Button.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.selected = false;
+ this.isMouseDown = false;
+
+ var ref = this;
+ if (window.PointerEvent) {
+ this.element.onpointerdown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onpointerup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onpointerover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onpointerout = function(e) { ref._mouseOutHandler(e); }
+
+ } else {
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ }
+ this.element.className = (!this.params.type == 'fill') ? "button" : "buttonOff";
+
+ return this;
+};
+
+Neo.Button.prototype._mouseDownHandler = function(e) {
+ if (Neo.painter.isUIPaused()) return;
+ this.isMouseDown = true;
+
+ if ((this.params.type == "fill") && (this.selected == false)) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.setSelected((this.selected) ? false : true);
+ }
+ Neo.painter.setToolByType(Neo.Painter.TOOLTYPE_FILL);
+ }
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+Neo.Button.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+Neo.Button.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.Button.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.Button.prototype.setSelected = function(selected) {
+ if (selected) {
+ this.element.className = "buttonOn";
+ } else {
+ this.element.className = "buttonOff";
+ }
+ this.selected = selected;
+};
+
+Neo.Button.prototype.update = function() {
+};
+
+/*
+-------------------------------------------------------------------------
+ ColorTip
+-------------------------------------------------------------------------
+*/
+
+Neo.colorTips = [];
+
+Neo.ColorTip = function() {};
+Neo.ColorTip.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ this.selected = (this.name == "color1") ? true : false;
+ this.isMouseDown = false;
+
+ var ref = this;
+ if (window.PointerEvent) {
+ this.element.onpointerdown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onpointerup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onpointerover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onpointerout = function(e) { ref._mouseOutHandler(e); }
+
+ } else {
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ }
+ this.element.className = "colorTipOff";
+
+ var index = parseInt(this.name.slice(5)) - 1;
+ this.element.style.left = (index % 2) ? "0px" : "26px";
+ this.element.style.top = Math.floor(index / 2) * 21 + "px";
+
+ // base64 ColorTip.png
+ this.element.innerHTML = "<img style='max-width:44px;' src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAASCAYAAAAg9DzcAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAANklEQVRIx+3OAQkAMADDsO3+Pe8qCj+0Akq6bQFqS2wTCpwE+R4IiyVYsGDBggULfirBgn8HX7BzCRwDx1QeAAAAAElFTkSuQmCC' />"
+
+ this.setColor(Neo.config.colors[params.index - 1]);
+
+ this.setSelected(this.selected);
+ Neo.colorTips.push(this);
+};
+
+Neo.ColorTip.prototype._mouseDownHandler = function(e) {
+ if (Neo.painter.isUIPaused()) return;
+ this.isMouseDown = true;
+
+ for (var i = 0; i < Neo.colorTips.length; i++) {
+ var colorTip = Neo.colorTips[i];
+ if (this == colorTip) {
+ if (e.shiftKey) {
+ this.setColor(Neo.config.colors[this.params.index - 1]);
+ } else if (e.button == 2 || e.ctrlKey || e.altKey) {
+ this.setColor(Neo.painter.foregroundColor);
+ }
+ }
+ colorTip.setSelected(this == colorTip) ? true : false;
+ }
+ Neo.painter.setColor(this.color);
+ Neo.updateUIColor(true, false);
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+Neo.ColorTip.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+Neo.ColorTip.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.ColorTip.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.ColorTip.prototype.setSelected = function(selected) {
+ if (selected) {
+ this.element.className = "colorTipOn";
+ } else {
+ this.element.className = "colorTipOff";
+ }
+ this.selected = selected;
+};
+
+Neo.ColorTip.prototype.setColor = function(color) {
+ this.color = color;
+ this.element.style.backgroundColor = color;
+};
+
+Neo.ColorTip.getCurrent = function() {
+ for (var i = 0; i < Neo.colorTips.length; i++) {
+ var colorTip = Neo.colorTips[i];
+ if (colorTip.selected) return colorTip;
+ }
+ return null;
+};
+
+/*
+-------------------------------------------------------------------------
+ ToolTip
+-------------------------------------------------------------------------
+*/
+
+Neo.toolTips = [];
+Neo.toolButtons = [];
+
+Neo.ToolTip = function() {};
+
+Neo.ToolTip.prototype.prevMode = -1;
+
+Neo.ToolTip.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.params.type = this.element.id;
+ this.name = name;
+ this.mode = 0;
+
+ this.isMouseDown = false;
+
+ var ref = this;
+ if (window.PointerEvent) {
+ this.element.onpointerdown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onpointerup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onpointerover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onpointerout = function(e) { ref._mouseOutHandler(e); }
+
+ } else {
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ }
+ this.selected = (this.params.type == "pen") ? true : false;
+ this.setSelected(this.selected);
+
+ this.element.innerHTML = "<canvas width=46 height=18></canvas><div class='label'></div>";
+ this.canvas = this.element.getElementsByTagName('canvas')[0];
+ this.label = this.element.getElementsByTagName('div')[0];
+
+ this.update();
+ return this;
+};
+
+Neo.ToolTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ if (this.isTool) {
+ if (this.selected == false) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.setSelected((this == toolTip) ? true : false);
+ }
+
+ } else {
+ var length = this.toolStrings.length;
+ if (e.button == 2 || e.ctrlKey || e.altKey) {
+ this.mode--;
+ if (this.mode < 0) this.mode = length - 1;
+ } else {
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ }
+ }
+ Neo.painter.setToolByType(this.tools[this.mode]);
+ this.update();
+ }
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+
+Neo.ToolTip.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+
+Neo.ToolTip.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.ToolTip.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.ToolTip.prototype.setSelected = function(selected) {
+ if (this.fixed) {
+ this.element.className = "toolTipFixed";
+
+ } else {
+ if (selected) {
+ this.element.className = "toolTipOn";
+ } else {
+ this.element.className = "toolTipOff";
+ }
+ }
+ this.selected = selected;
+};
+
+Neo.ToolTip.prototype.update = function() {};
+
+Neo.ToolTip.prototype.draw = function(c) {
+ if (this.hasTintImage) {
+ if (typeof c != "string") c = Neo.painter.getColorString(c);
+ var ctx = this.canvas.getContext("2d");
+
+ if (this.prevMode != this.mode) {
+ this.prevMode = this.mode;
+
+ var img = new Image();
+ img.src = this.toolIcons[this.mode];
+ img.onload = function() {
+ var ref = this;
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.drawTintImage(ctx, img, c, 0, 0);
+ }.bind(this);
+
+ } else {
+ this.tintImage(ctx, c);
+ }
+ }
+};
+
+Neo.ToolTip.prototype.drawTintImage = function(ctx, img, c, x, y) {
+ ctx.drawImage(img, x, y);
+ this.tintImage(ctx, c);
+};
+
+Neo.ToolTip.prototype.tintImage = function(ctx, c) {
+ c = (Neo.painter.getColor(c) & 0xffffff);
+
+ var imageData = ctx.getImageData(0, 0, 46, 18);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ for (var i = 0; i < buf32.length; i++) {
+ var a = buf32[i] & 0xff000000;
+ if (a) {
+ buf32[i] = buf32[i] & a | c;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+};
+
+Neo.ToolTip.bezier = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAT0lEQVRIx+3SQQoAIAhE0en+h7ZVEEKBZrX5b5sjKknAkRYpNslaMLPq44ZI9wwHs0vMQ/v87u0Kk8xfsaI242jbMdjPi5Y0r/zTAAAAD3UOjRf9jcO4sgAAAABJRU5ErkJggg==";
+Neo.ToolTip.blur = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAASUlEQVRIx+3VMQ4AIAgEQeD/f8bWWBnJYUh2SgtgK82G8/MhzVKwxOtTLgIUx6tDout4laiPIICA0Qj4bXxAy0+8LZP9yACAJwsqkggS55eiZgAAAABJRU5ErkJggg==";
+Neo.ToolTip.blurrect = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAX0lEQVRIx+2XQQ4AEAwEt+I7/v+8Org6lJKt6NzLjjYE8DAKtLpYoDeCCCC7tYUd3ru2qQOzDTyndhJzB6KSAmxSgM0fAlGuzBnmlziqxB8jFJkUYJMCbAQYPxt2kF06fvYKgjPBO/IAAAAASUVORK5CYII=";
+Neo.ToolTip.brush = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAQUlEQVRIx2NgGOKAEcb4z8CweRA4xpdUPSxofJ8BdP8WcjQxDaCDqQLQY4CsUBgFo2AUjIJRMApGwSgYBaNgZAIA0CoDwDbZu8oAAAAASUVORK5CYII=";
+Neo.ToolTip.burn = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAPklEQVRIx+3PMRIAMAQAQbzM0/0sKZPeiDG57TQ4keH0Htx9VR+MCM1vOezl8xUsv4IAAkYjoBsB3QgAgL9tYXgF19rh9yoAAAAASUVORK5CYII=";
+Neo.ToolTip.copy = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAW0lEQVRIx+2XMQoAIAwDU/E7/v95Orh2KMUSC7m5Qs6AUqAxG1gzOLirwxhgmXOjOlg1oQY8sjf2mvYNSICNBNhIgE3oH/jlzfdo34AE2EiATXsBA+5mww6S5QASDwSGMt8ouwAAAABJRU5ErkJggg==";
+Neo.ToolTip.copy2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAN0lEQVRIx+3PwQkAIBADwdPKt3MtQVCOPNz5B7JV0pNxOwRW9zng+G92n+hmQJoBaQakGSBJf9tyBgQUV/fKCAAAAABJRU5ErkJggg==";
+Neo.ToolTip.ellipse = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAATklEQVRIx+2VMQ4AIAgD6/8fjbOJi1LFmt4OPQ0KIE7LNgggCBLbHkuFM9lM+Om+QwDjpksyb4tT86vlvzgEbYxefQPyv5D8HjDGGGOk6b3jJ+lYubd8AAAAAElFTkSuQmCC";
+Neo.ToolTip.ellipsefill = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAVUlEQVRIx+2VURIAEAgFc/9D5waSHpV5+43ZHRMizRnRA1REARLHHq6NCFl01Nail+LeEDMgU34nYhlQQd6K+PsGKkSEZyArBPoK3Y6K/AOEEEJIayZHbhIKjkZrFwAAAABJRU5ErkJggg==";
+Neo.ToolTip.eraser = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAABQElEQVRIx+1WQY7CMAwcI37Cad+yXOgH4Gu8gAt9CtrDirfMHjZJbbcktVSpQnROSeMkY3vsFHhzSG3xfLpz/JVmG0mIqDkIMcc6+7Kejx6fdb0dq7w09rVFkrjejrMOunQ9vg7f/5QEIAd6E1Eo38WF8fF7n8sdALCrLerIzoFI4sI0Vtv1SYZ8CVbeF7tzF7JugIkVkxOauc6CIe8842S+XmMfsq7TN9LRTngZmTmVD4SrnzYaGYhFoxCWgajXuMjYGTuJ3dlwIBIN3U0cUVqLXCs5E7YeVsvAYJul5HWeLUhL3EpstQwooqoOTEHDOebpMn7ngkUsg3RotU8X1MkuVDrYohkIupC0YArX6T+PfX3kcbQLNV/iCKi6EB3xqXdAZ0JKthZ8B0QEl673NIEX/0I/z36Rf6ENGzZ8EP4A8Lp+9e9VWC4AAAAASUVORK5CYII=";
+Neo.ToolTip.flip = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAZklEQVRIx+2XQQoAIAgE1+g7/f95degWHSyTTXDOhTsSiUBgOtCq8mD3DiOA3NxTCVgKaLA0qHiFOsHSnC8ELKQAmxRgE15APQfWv9pzLjwX+CXsjvBPKAXYpACb8AICzM2GHeSWAfVOCIiJuQ9tAAAAAElFTkSuQmCC";
+Neo.ToolTip.freehand = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAdUlEQVRIx+2WUQrAMAhD3dj9r+y+VoSyLhYDynzQv1qiJlCR4hzeAhVRsiC3Jkj0c5hN7Lx7IQ9SphLE1ICdwko420purEWQuywN3pqxgcw2+WwAtU1GzoqiLZNwZBvMAIcO8y3YKUO8mkbmjPzjK9E0TUPjBoeyLAS0usjLAAAAAElFTkSuQmCC";
+Neo.ToolTip.line = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAU0lEQVRIx+2UQQ4AIAjD8P+PxivRGDQC47C+oN1hIgTLQAt4qIga2c23XYAVPkm3CVhlb4ShAa/rQgMi1i0NyFg3LaBq3bAA1LpfAd7/EkIIIR2YXFYSCpWS8w8AAAAASUVORK5CYII=";
+Neo.ToolTip.merge = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAW0lEQVRIx+2XQQrAQAgDx9Lv9JF9+e6h54IINlgyZ4UMOYgwmAXXmRxc3WECorJ3dAfrJtXAC7c6PPygAQuosYAaC6hJ3YHqlfyC8Q1YQI0F1IwXCHg+G3WQKhvwgwUFmFyYbwAAAABJRU5ErkJggg==";
+Neo.ToolTip.pen = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAK0lEQVRIx+3OsQkAMAwDQXn/oe3WfSAEctd9I5TA32pHJ/3AoTpfAQCAGwaa5AICJLKWSQAAAABJRU5ErkJggg==";
+Neo.ToolTip.rect = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAQElEQVRIx+3TMQ4AIAhD0WK8/5VxdcIYY8rw3wok7YAEr6iGKaU74BY0ro+6FKhyDHe4VxRwm6eFLn8AAADwwQIwTQgGo9ZMywAAAABJRU5ErkJggg==";
+Neo.ToolTip.rectfill = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAANElEQVRIx+3PIQ4AIBADwcL//3xYBMEgLiQztmab0GvcxkqqO3ALPbbO7rBXDnRzAADgYwvqDwIMJlGb5QAAAABJRU5ErkJggg==";
+Neo.ToolTip.text = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAcUlEQVRIx+2VwQ7AIAhDy7L//2V2WmIYg+ky2KEv8aCCqYQqQMgrJNpUQMXEKKDmAPHyspgSrBBvLZu3cQqZEdwhfusq0KdkVR5HlFfBvpI0mtIzeusFot7vFPqYuzZYMXUFlzc+qrIn7tf/ACGEkIwDlEQ94YZjzcgAAAAASUVORK5CYII=";
+Neo.ToolTip.tone = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAO0lEQVRIx+3PIQ4AMAgEwaP//zNVVZUELiQ7CgWstFy8IaVsPhT1Lb/T+fQEAtwIcCPAjQC39QEAgJIL6DQCFhAqsRkAAAAASUVORK5CYII=";
+
+/*
+-------------------------------------------------------------------------
+ PenTip
+-------------------------------------------------------------------------
+*/
+
+Neo.penTip;
+
+Neo.PenTip = function() {};
+Neo.PenTip.prototype = new Neo.ToolTip();
+
+Neo.PenTip.prototype.toolStrings = ["Lapiz", "Acuarela", "Texto"];
+Neo.PenTip.prototype.tools = [Neo.Painter.TOOLTYPE_PEN,
+ Neo.Painter.TOOLTYPE_BRUSH,
+ Neo.Painter.TOOLTYPE_TEXT];
+
+Neo.PenTip.prototype.hasTintImage = true;
+Neo.PenTip.prototype.toolIcons = [Neo.ToolTip.pen,
+ Neo.ToolTip.brush,
+ Neo.ToolTip.text];
+
+Neo.PenTip.prototype.init = function(name, params) {
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.PenTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ if (this.label) {
+ this.label.innerHTML = this.toolStrings[this.mode];
+ }
+};
+
+/*
+-------------------------------------------------------------------------
+ Pen2Tip
+-------------------------------------------------------------------------
+*/
+
+Neo.pen2Tip;
+
+Neo.Pen2Tip = function() {};
+Neo.Pen2Tip.prototype = new Neo.ToolTip();
+
+Neo.Pen2Tip.prototype.toolStrings = ["Tono",
+ "Gradacion",
+ "Sobreexp.",
+ "Quemar"];
+Neo.Pen2Tip.prototype.tools = [Neo.Painter.TOOLTYPE_TONE,
+ Neo.Painter.TOOLTYPE_BLUR,
+ Neo.Painter.TOOLTYPE_DODGE,
+ Neo.Painter.TOOLTYPE_BURN];
+
+Neo.Pen2Tip.prototype.hasTintImage = true;
+Neo.Pen2Tip.prototype.toolIcons = [Neo.ToolTip.tone,
+ Neo.ToolTip.blur,
+ Neo.ToolTip.burn,
+ Neo.ToolTip.burn];
+
+Neo.Pen2Tip.prototype.init = function(name, params) {
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.Pen2Tip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ switch (this.tools[this.mode]) {
+ case Neo.Painter.TOOLTYPE_TONE:
+ this.drawTone(Neo.painter.foregroundColor);
+ break;
+
+ case Neo.Painter.TOOLTYPE_DODGE:
+ this.draw(0xffc0c0c0);
+ break;
+
+ case Neo.Painter.TOOLTYPE_BURN:
+ this.draw(0xff404040);
+ break;
+
+ default:
+ this.draw(Neo.painter.foregroundColor);
+ break;
+ }
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.Pen2Tip.prototype.drawTone = function() {
+ var ctx = this.canvas.getContext("2d");
+
+ var imageData = ctx.getImageData(0, 0, 46, 18);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var c = Neo.painter.getColor() | 0xff000000;
+ var a = Math.floor(Neo.painter.alpha * 255);
+ var toneData = Neo.painter.getToneData(a);
+
+ for (var j = 0; j < 18; j++) {
+ for (var i = 0; i < 46; i++) {
+ if (j >= 1 && j < 12 &&
+ i >= 2 && i < 26 &&
+ toneData[(i%4) + (j%4) * 4]) {
+ buf32[j * 46 + i] = c;
+
+ } else {
+ buf32[j * 46 + i] = 0;
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+
+ this.prevMode = this.mode;
+};
+
+
+/*
+-------------------------------------------------------------------------
+ EraserTip
+-------------------------------------------------------------------------
+*/
+
+Neo.eraserTip;
+
+Neo.EraserTip = function() {};
+Neo.EraserTip.prototype = new Neo.ToolTip();
+
+Neo.EraserTip.prototype.toolStrings = ["Goma", "Goma", "Borrar"];
+Neo.EraserTip.prototype.tools = [Neo.Painter.TOOLTYPE_ERASER,
+ Neo.Painter.TOOLTYPE_ERASERECT,
+ Neo.Painter.TOOLTYPE_ERASEALL];
+
+Neo.EraserTip.prototype.init = function(name, params) {
+ this.drawOnce = false;
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.EraserTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ if (this.drawOnce == false) {
+ this.draw();
+ this.drawOnce = true;
+ }
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.EraserTip.prototype.draw = function() {
+ var ctx = this.canvas.getContext("2d");
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ var img = new Image();
+
+ img.src = Neo.ToolTip.eraser;
+ img.onload = function() {
+ ctx.drawImage(img, 0, 0);
+ };
+};
+
+/*
+-------------------------------------------------------------------------
+ EffectTip
+-------------------------------------------------------------------------
+*/
+
+Neo.effectTip;
+
+Neo.EffectTip = function() {};
+Neo.EffectTip.prototype = new Neo.ToolTip();
+
+Neo.EffectTip.prototype.toolStrings = ["Cuadrado", "Cuadrado", "Circulo", "Circulo"];
+Neo.EffectTip.prototype.tools = [Neo.Painter.TOOLTYPE_RECTFILL,
+ Neo.Painter.TOOLTYPE_RECT,
+ Neo.Painter.TOOLTYPE_ELLIPSEFILL,
+ Neo.Painter.TOOLTYPE_ELLIPSE];
+
+Neo.EffectTip.prototype.hasTintImage = true;
+Neo.EffectTip.prototype.toolIcons = [Neo.ToolTip.rectfill,
+ Neo.ToolTip.rect,
+ Neo.ToolTip.ellipsefill,
+ Neo.ToolTip.ellipse];
+
+Neo.EffectTip.prototype.init = function(name, params) {
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.EffectTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+-------------------------------------------------------------------------
+ Effect2Tip
+-------------------------------------------------------------------------
+*/
+
+Neo.effect2Tip;
+
+Neo.Effect2Tip = function() {};
+Neo.Effect2Tip.prototype = new Neo.ToolTip();
+
+Neo.Effect2Tip.prototype.toolStrings = ["Copiar", "Unir",
+ "Cortar",
+ "Inv. Izq/Der", "Inv. Arr/Aba", "Inclinar"];
+Neo.Effect2Tip.prototype.tools = [Neo.Painter.TOOLTYPE_COPY,
+ Neo.Painter.TOOLTYPE_MERGE,
+ Neo.Painter.TOOLTYPE_BLURRECT,
+ Neo.Painter.TOOLTYPE_FLIP_H,
+ Neo.Painter.TOOLTYPE_FLIP_V,
+ Neo.Painter.TOOLTYPE_TURN];
+
+Neo.Effect2Tip.prototype.hasTintImage = true;
+Neo.Effect2Tip.prototype.toolIcons = [Neo.ToolTip.copy,
+ Neo.ToolTip.merge,
+ Neo.ToolTip.blurrect,
+ Neo.ToolTip.flip,
+ Neo.ToolTip.flip,
+ Neo.ToolTip.flip];
+
+Neo.Effect2Tip.prototype.init = function(name, params) {
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+
+ this.img = document.createElement("img");
+ this.img.src = Neo.ToolTip.copy2;
+ this.element.appendChild(this.img);
+ return this;
+};
+
+Neo.Effect2Tip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+-------------------------------------------------------------------------
+ MaskTip
+-------------------------------------------------------------------------
+*/
+
+Neo.maskTip;
+
+Neo.MaskTip = function() {};
+Neo.MaskTip.prototype = new Neo.ToolTip();
+
+Neo.MaskTip.prototype.toolStrings = ["Normal", "Masc.", "Masc. Inv.", "Adicion", "Substrac"];
+
+Neo.MaskTip.prototype.init = function(name, params) {
+ this.fixed = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.MaskTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ if (e.button == 2 || e.ctrlKey || e.altKey) {
+ Neo.painter.maskColor = Neo.painter.foregroundColor;
+
+ } else {
+ var length = this.toolStrings.length;
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ Neo.painter.maskType = this.mode;
+ }
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+}
+
+Neo.MaskTip.prototype.update = function() {
+ this.draw(Neo.painter.maskColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.MaskTip.prototype.draw = function(c) {
+ if (typeof c != "string") c = Neo.painter.getColorString(c);
+
+ var ctx = this.canvas.getContext("2d");
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ ctx.fillStyle = c;
+ ctx.fillRect(1, 1, 43, 9);
+};
+
+/*
+-------------------------------------------------------------------------
+ DrawTip
+-------------------------------------------------------------------------
+*/
+
+Neo.drawTip;
+
+Neo.DrawTip = function() {};
+Neo.DrawTip.prototype = new Neo.ToolTip();
+
+Neo.DrawTip.prototype.toolStrings = ["Libre", "Linea", "Curva"];
+
+Neo.DrawTip.prototype.hasTintImage = true;
+Neo.DrawTip.prototype.toolIcons = [Neo.ToolTip.freehand,
+ Neo.ToolTip.line,
+ Neo.ToolTip.bezier];
+
+Neo.DrawTip.prototype.init = function(name, params) {
+ this.fixed = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.DrawTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ var length = this.toolStrings.length;
+ if (e.button == 2 || e.ctrlKey || e.altKey) {
+ this.mode--;
+ if (this.mode < 0) this.mode = length - 1;
+ } else {
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ }
+ Neo.painter.drawType = this.mode;
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+}
+
+Neo.DrawTip.prototype.update = function() {
+ this.mode = Neo.painter.drawType;
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+-------------------------------------------------------------------------
+ ColorSlider
+-------------------------------------------------------------------------
+*/
+
+Neo.sliders = [];
+
+Neo.ColorSlider = function() {};
+
+Neo.ColorSlider.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+ this.value = 0;
+ this.type = this.params.type;
+
+ this.element.className = "colorSlider";
+ this.element.innerHTML = "<div class='slider'></div><div class='label'></div>";
+ this.element.innerHTML += "<div class='hit'></div>";
+
+ this.slider = this.element.getElementsByClassName('slider')[0];
+ this.label = this.element.getElementsByClassName('label')[0];
+ this.hit = this.element.getElementsByClassName('hit')[0];
+ this.hit['data-slider'] = params.type;
+
+ switch (this.type) {
+ case Neo.SLIDERTYPE_RED:
+ this.prefix = "R";
+ this.slider.style.backgroundColor = "#fa9696";
+ break;
+ case Neo.SLIDERTYPE_GREEN:
+ this.prefix = "G";
+ this.slider.style.backgroundColor = "#82f238";
+ break;
+ case Neo.SLIDERTYPE_BLUE:
+ this.prefix = "B";
+ this.slider.style.backgroundColor = "#8080ff";
+ break;
+ case Neo.SLIDERTYPE_ALPHA:
+ this.prefix = "A";
+ this.slider.style.backgroundColor = "#aaaaaa";
+ this.value = 255;
+ break;
+ }
+
+ this.update();
+ return this;
+};
+
+Neo.ColorSlider.prototype.downHandler = function(x, y) {
+ if (Neo.painter.isShiftDown) {
+ this.shift(x, y);
+
+ } else {
+ this.slide(x, y);
+ }
+};
+
+Neo.ColorSlider.prototype.moveHandler = function(x, y) {
+ this.slide(x, y);
+};
+
+Neo.ColorSlider.prototype.upHandler = function(x, y) {
+};
+
+Neo.ColorSlider.prototype.shift = function(x, y) {
+ var value;
+ if (x >= 0 && x < 60 && y >= 0 && y <= 15) {
+ var v = Math.floor((x - 5) * 5.0);
+ var min = (this.type == Neo.SLIDERTYPE_ALPHA) ? 1 : 0;
+
+ value = Math.max(Math.min(v, 255), min);
+ if (this.value > value || this.value == 255) {
+ this.value--;
+ } else {
+ this.value++;
+ }
+ this.value = Math.max(Math.min(this.value, 255), min);
+ this.value0 = this.value;
+ this.x0 = x;
+ }
+
+ if (this.type == Neo.SLIDERTYPE_ALPHA) {
+ Neo.painter.alpha = this.value / 255.0;
+ this.update();
+ Neo.updateUIColor(false, false);
+
+ } else {
+ var r = Neo.sliders[Neo.SLIDERTYPE_RED].value;
+ var g = Neo.sliders[Neo.SLIDERTYPE_GREEN].value;
+ var b = Neo.sliders[Neo.SLIDERTYPE_BLUE].value;
+
+ Neo.painter.setColor(r<<16 | g<<8 | b);
+ Neo.updateUIColor(true, true);
+ }
+};
+
+Neo.ColorSlider.prototype.slide = function(x, y) {
+ var value;
+ if (x >= 0 && x < 60 && y >= 0 && y <= 15) {
+ var v = Math.floor((x - 5) * 5.0);
+ value = Math.round(v / 5) * 5;
+
+ this.value0 = value;
+ this.x0 = x;
+
+ } else {
+ var d = (x - this.x0) / 3.0;
+ value = this.value0 + d;
+ }
+
+ var min = (this.type == Neo.SLIDERTYPE_ALPHA) ? 1 : 0;
+ this.value = Math.max(Math.min(value, 255), min);
+
+ if (this.type == Neo.SLIDERTYPE_ALPHA) {
+ Neo.painter.alpha = this.value / 255.0;
+ this.update();
+ Neo.updateUIColor(false, false);
+
+ } else {
+ var r = Neo.sliders[Neo.SLIDERTYPE_RED].value;
+ var g = Neo.sliders[Neo.SLIDERTYPE_GREEN].value;
+ var b = Neo.sliders[Neo.SLIDERTYPE_BLUE].value;
+
+ Neo.painter.setColor(r<<16 | g<<8 | b);
+// Neo.updateUIColor(true, true);
+ }
+};
+
+Neo.ColorSlider.prototype.update = function() {
+ var color = Neo.painter.getColor();
+ var alpha = Neo.painter.alpha * 255;
+
+ switch (this.type) {
+ case Neo.SLIDERTYPE_RED: this.value = (color & 0x0000ff); break;
+ case Neo.SLIDERTYPE_GREEN: this.value = (color & 0x00ff00) >> 8; break;
+ case Neo.SLIDERTYPE_BLUE: this.value = (color & 0xff0000) >> 16; break;
+ case Neo.SLIDERTYPE_ALPHA: this.value = alpha; break;
+ }
+
+ var width = this.value * 49.0 / 255.0;
+ width = Math.max(Math.min(48, width), 1);
+
+ this.slider.style.width = width.toFixed(2) + "px";
+ this.label.innerHTML = this.prefix + this.value.toFixed(0);
+};
+
+/*
+-------------------------------------------------------------------------
+ SizeSlider
+-------------------------------------------------------------------------
+*/
+
+Neo.SizeSlider = function() {};
+
+Neo.SizeSlider.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+ this.value = this.value0 = 1;
+
+ this.element.className = "sizeSlider";
+ this.element.innerHTML = "<div class='slider'></div><div class='label'></div>";
+ this.element.innerHTML += "<div class='hit'></div>"
+
+ this.slider = this.element.getElementsByClassName('slider')[0];
+ this.label = this.element.getElementsByClassName('label')[0];
+ this.hit = this.element.getElementsByClassName('hit')[0];
+ this.hit['data-slider'] = params.type;
+
+ this.slider.style.backgroundColor = Neo.painter.foregroundColor;
+ this.update();
+ return this;
+};
+
+Neo.SizeSlider.prototype.downHandler = function(x, y) {
+ if (Neo.painter.isShiftDown) {
+ this.shift(x, y);
+
+ } else {
+ this.value0 = this.value;
+ this.y0 = y;
+ this.slide(x, y);
+ }
+};
+
+Neo.SizeSlider.prototype.moveHandler = function(x, y) {
+ this.slide(x, y);
+};
+
+Neo.SizeSlider.prototype.upHandler = function(x, y) {
+};
+
+Neo.SizeSlider.prototype.shift = function(x, y) {
+ var value0 = Neo.painter.lineWidth;
+ var value;
+
+ if (!Neo.painter.tool.alt) {
+ var v = Math.floor((y - 4) * 30.0 / 33.0);
+
+ value = Math.max(Math.min(v, 30), 1);
+ if (value0 > value || value0 == 30) {
+ value0--;
+ } else {
+ value0++;
+ }
+ this.setSize(value0);
+ }
+};
+
+Neo.SizeSlider.prototype.slide = function(x, y) {
+ var value;
+ if (!Neo.painter.tool.alt) {
+ if (x >= 0 && x < 48 && y >= 0 && y < 41) {
+ var v = Math.floor((y - 4) * 30.0 / 33.0);
+ value = v;
+
+ this.value0 = value;
+ this.y0 = y;
+
+ } else {
+ var d = (y - this.y0) / 7.0;
+ value = this.value0 + d;
+ }
+ } else {
+ // Ctrl+Alt+ドラッグã§ã‚µã‚¤ã‚ºå¤‰æ›´ã™ã‚‹ã¨ã
+ var d = y - this.y0;
+ value = this.value0 + d;
+ }
+
+ value = Math.max(Math.min(value, 30), 1);
+ this.setSize(value);
+};
+
+Neo.SizeSlider.prototype.setSize = function(value) {
+ value = Math.round(value);
+ Neo.painter.lineWidth = Math.max(Math.min(30, value), 1);
+
+ var tool = Neo.painter.getCurrentTool();
+ if (tool) {
+ if (tool.type == Neo.Painter.TOOLTYPE_BRUSH) {
+ Neo.painter.alpha = tool.getAlpha();
+ Neo.sliders[Neo.SLIDERTYPE_ALPHA].update();
+
+ } else if (tool.type == Neo.Painter.TOOLTYPE_TEXT) {
+ Neo.painter.updateInputText();
+ }
+ }
+ this.update();
+};
+
+Neo.SizeSlider.prototype.update = function() {
+ this.value = Neo.painter.lineWidth;
+
+ var height = this.value * 33.0 / 30.0;
+ height = Math.max(Math.min(34, height), 1);
+
+ this.slider.style.height = height.toFixed(2) + "px";
+ this.label.innerHTML = this.value + "px";
+ this.slider.style.backgroundColor = Neo.painter.foregroundColor;
+};
+
+/*
+-------------------------------------------------------------------------
+ LayerControl
+-------------------------------------------------------------------------
+*/
+
+Neo.LayerControl = function() {};
+Neo.LayerControl.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+
+ var ref = this;
+
+ var mousedown = (window.PointerEvent) ? "onpointerdown" : "onmousedown";
+ this.element[mousedown] = function(e) { ref._mouseDownHandler(e); }
+ this.element.className = "layerControl";
+ this.element.innerHTML = "<div class='bg'></div><div class='label0'>Capa0</div><div class='label1'>Capa1</div><div class='line1'></div><div class='line0'></div>";
+
+ this.bg = this.element.getElementsByClassName('bg')[0];
+ this.label0 = this.element.getElementsByClassName('label0')[0];
+ this.label1 = this.element.getElementsByClassName('label1')[0];
+ this.line0 = this.element.getElementsByClassName('line0')[0];
+ this.line1 = this.element.getElementsByClassName('line1')[0];
+
+ this.line0.style.display = "none";
+ this.line1.style.display = "none";
+ this.label1.style.display = "none";
+
+ this.update();
+ return this;
+};
+
+Neo.LayerControl.prototype._mouseDownHandler = function(e) {
+ if (e.button == 2 || e.ctrlKey || e.altKey) {
+ var visible = Neo.painter.visible[Neo.painter.current];
+ Neo.painter.visible[Neo.painter.current] = (visible) ? false : true;
+
+ } else {
+ var current = Neo.painter.current;
+ Neo.painter.current = (current) ? 0 : 1
+ }
+ Neo.painter.updateDestCanvas(0, 0, Neo.painter.canvasWidth, Neo.painter.canvasHeight);
+ if (Neo.painter.tool.type == Neo.Painter.TOOLTYPE_PASTE) {
+ Neo.painter.tool.drawCursor(Neo.painter);
+ }
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+
+Neo.LayerControl.prototype.update = function() {
+ this.label0.style.display = (Neo.painter.current == 0) ? "block" : "none";
+ this.label1.style.display = (Neo.painter.current == 1) ? "block" : "none";
+ this.line0.style.display = (Neo.painter.visible[0]) ? "none" : "block";
+ this.line1.style.display = (Neo.painter.visible[1]) ? "none" : "block";
+};
+
+/*
+-------------------------------------------------------------------------
+ ReserveControl
+-------------------------------------------------------------------------
+*/
+Neo.reserveControls = [];
+
+Neo.ReserveControl = function() {};
+Neo.ReserveControl.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ var ref = this;
+
+ var mousedown = (window.PointerEvent) ? "onpointerdown" : "onmousedown";
+ this.element[mousedown] = function(e) { ref._mouseDownHandler(e); }
+ this.element.className = "reserve";
+
+ var index = parseInt(this.name.slice(7)) - 1;
+ this.element.style.top = "1px";
+ this.element.style.left = (index * 15 + 2) + "px";
+
+ this.reserve = Neo.clone(Neo.config.reserves[index]);
+ this.update();
+
+ Neo.reserveControls.push(this);
+ return this;
+};
+
+Neo.ReserveControl.prototype._mouseDownHandler = function(e) {
+ if (e.button == 2 || e.ctrlKey || e.altKey) {
+ this.save();
+ } else {
+ this.load();
+ }
+ this.update();
+};
+
+Neo.ReserveControl.prototype.load = function() {
+ Neo.painter.setToolByType(this.reserve.tool)
+ Neo.painter.foregroundColor = this.reserve.color;
+ Neo.painter.lineWidth = this.reserve.size;
+ Neo.painter.alpha = this.reserve.alpha;
+
+ switch (this.reserve.tool) {
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TONE:
+ Neo.painter.drawType = this.reserve.drawType;
+ };
+ Neo.updateUI();
+};
+
+Neo.ReserveControl.prototype.save = function() {
+ this.reserve.color = Neo.painter.foregroundColor;
+ this.reserve.size = Neo.painter.lineWidth;
+ this.reserve.drawType = Neo.painter.drawType;
+ this.reserve.alpha = Neo.painter.alpha;
+ this.reserve.tool = Neo.painter.tool.getType();
+ this.element.style.backgroundColor = this.reserve.color;
+ this.update();
+ Neo.updateUI();
+};
+
+Neo.ReserveControl.prototype.update = function() {
+ this.element.style.backgroundColor = this.reserve.color;
+};
+
+/*
+-------------------------------------------------------------------------
+ ScrollBarButton
+-------------------------------------------------------------------------
+*/
+
+Neo.scrollH;
+Neo.scrollV;
+
+Neo.ScrollBarButton = function() {};
+Neo.ScrollBarButton.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ this.element.innerHTML = "<div></div>";
+ this.barButton = this.element.getElementsByTagName("div")[0];
+ this.element['data-bar'] = true;
+ this.barButton['data-bar'] = true;
+
+ if (name == "scrollH") Neo.scrollH = this;
+ if (name == "scrollV") Neo.scrollV = this;
+ return this;
+};
+
+Neo.ScrollBarButton.prototype.update = function(oe) {
+ if (this.name == "scrollH") {
+ var a = oe.destCanvas.width / (oe.canvasWidth * oe.zoom);
+ var barWidth = Math.ceil(oe.destCanvas.width * a);
+ var barX = (oe.scrollBarX) * (oe.destCanvas.width - barWidth);
+ this.barButton.style.width = (Math.ceil(barWidth) - 4) + "px";
+ this.barButton.style.left = Math.floor(barX) + "px";
+
+ } else {
+ var a = oe.destCanvas.height / (oe.canvasHeight * oe.zoom);
+ var barHeight = Math.ceil(oe.destCanvas.height * a);
+ var barY = (oe.scrollBarY) * (oe.destCanvas.height - barHeight);
+ this.barButton.style.height = (Math.ceil(barHeight) - 4) + "px";
+ this.barButton.style.top = Math.floor(barY) + "px";
+ }
+};
+
diff --git a/static/js/paintbbs/PaintBBS-1.3.4.css b/static/js/paintbbs/PaintBBS-1.3.4.css
new file mode 100644
index 0000000..2e5b4f7
--- /dev/null
+++ b/static/js/paintbbs/PaintBBS-1.3.4.css
@@ -0,0 +1,547 @@
+.NEO {
+ margin:0;
+ line-height:18px;
+
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+ touch-callout: none;
+ -webkit-touch-callout: none;
+}
+
+.NEO *:active,
+.NEO *:hover {
+ cursor: default;
+}
+
+.NEO #container{
+ width: 100%;
+ height: 100%;
+ position: relative;
+ border: 1px dotted transparent;
+}
+
+.NEO #center{
+ display: table;
+ position: relative;
+ height: 100%;
+ margin: auto;
+}
+
+.NEO #toolsWrapper{
+ width: 52px;
+ display: table;
+ position:absolute;
+ top: 0;
+ right: -3px;
+}
+
+.NEO #tools{
+ width: 52px;
+ display: table-cell;
+ vertical-align: middle;
+ position: relative;
+}
+
+.NEO #painterContainer{
+ display:table-cell;
+ vertical-align: middle;
+ position: relative;
+}
+
+.NEO #painterWrapper{
+ padding: 0 55px 0 0;
+ position: relative;
+
+ float: right; /* ãªãœã‹Chrome55ã§ãšã‚Œã‚‹ã®ã§å¯¾ç­– */
+}
+
+.NEO #upper {
+ position: absolute;
+ right: 0;
+
+ height:30px;
+ padding-right: 75px; //20px;
+ text-align:right;
+}
+
+.NEO #lower {
+ height:30px;
+}
+
+.NEO #painter {
+ position: relative;
+ padding: 20px 20px 20px 20px;
+
+ margin-top: 30px;
+}
+
+.NEO #canvas {
+ position: relative;
+ width: 300px;
+ height: 300px;
+ background-color: white;
+}
+
+.NEO #headerButtons {
+ position: absolute;
+ top: 5px;
+ left: 5px;
+}
+
+.NEO #footerButtons {
+ position: absolute;
+ bottom: 5px;
+ left: 5px;
+}
+
+.NEO #window { float: left; }
+
+/*
+-----------------------
+ Modal Window
+-----------------------
+*/
+
+.NEO #pageView{
+ background-color: white;
+}
+
+.NEO #windowView{
+ z-index: 9999;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: white;
+}
+
+/*
+-----------------------
+ Controls
+-----------------------
+*/
+
+.NEO #zoomMinus {
+ padding: 0;
+ margin: 0;
+ width:16px;
+ height:16px;
+ margin-left:1px;
+}
+
+.NEO #zoomPlus {
+ padding: 0;
+ margin: 0;
+ width:16px;
+ height:16px;
+}
+
+.NEO #zoomMinusWrapper {
+ position: absolute;
+ right: -20px;
+ bottom: -21px;
+ width: 19px;
+ height: 19px;
+ text-align:center;
+}
+
+.NEO #zoomPlusWrapper {
+ position: absolute;
+ left: -21px;
+ bottom: -21px;
+ width: 19px;
+ height: 19px;
+ text-overflow: hidden;
+ text-align:center;
+}
+
+.NEO #scrollH {
+ position: absolute;
+ left: 1px;
+ bottom: -20px;
+ width: calc(100% - 2px);
+ height: 18px;
+}
+
+.NEO #scrollV {
+ position: absolute;
+ right: -20px;
+ top: 1px;
+ width: 18px;
+ height: calc(100% - 2px);
+}
+
+.NEO #scrollH div {
+ position: absolute;
+ width: 50px;
+ height: 16px;
+}
+
+.NEO #scrollV div {
+ position: absolute;
+ width: 16px;
+ height: 50px;
+}
+
+.NEO #scrollH div:hover {}
+.NEO #scrollV div:hover {}
+
+.NEO #neoWarning {
+ position: absolute;
+ left: 5px;
+ top: 5px;
+ color: red;
+ pointer-events: none;
+ text-align: left;
+ transition: all 2s;
+}
+
+/*
+-----------------------
+ Widgets
+-----------------------
+*/
+
+.NEO .buttonOff {
+ display: inline-block;
+ border: 1px solid white;
+ height: 19px;
+ padding: 3px;
+ height: 16px;
+ font-size: 15px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ margin-right: 4px;
+
+ border-left: 1px solid rgba(0, 0, 0, 0);
+ border-top: 1px solid rgba(0, 0, 0, 0);
+ border-right: 1px solid rgba(0, 0, 0, 0);
+ border-bottom: 1px solid rgba(0, 0, 0, 0);
+
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ margin: 1px;
+
+}
+
+.NEO .buttonOff:hover {}
+
+.NEO .buttonOff:active,
+.NEO .buttonOn {
+ display: inline-block;
+ height: 19px;
+ padding: 3px;
+ height: 16px;
+ font-size: 15px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ margin-right: 4px;
+
+ border-right: 1px solid rgba(0, 0, 0, 0);
+ border-bottom: 1px solid rgba(0, 0, 0, 0);
+
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ margin: 1px;
+}
+
+.NEO #right { display: inline-block; float: right; margin-left: 10px; }
+
+.NEO .toolTipOff,
+.NEO .toolTipFixed {
+ position:relative;
+ padding: 0;
+ margin: 0;
+ margin-top: 3px;
+ margin-bottom: 3px;
+
+ width: 46px;
+ height: 18px;
+ border-top: 1px solid #ffffff;
+ border-left: 1px solid #ffffff;
+ border-right: 1px solid #9397b2;
+ border-bottom: 1px solid #9397b2;
+}
+
+.NEO .toolTipOn {
+ position:relative;
+ padding: 0;
+ margin: 0;
+ margin-top: 3px;
+ margin-bottom: 3px;
+
+ width: 46px;
+ height: 18px;
+ border-top: 1px solid rgba(0, 0, 0, 0);
+ border-left: 1px solid rgba(0, 0, 0, 0);
+ border-right: 1px solid #ffffff;
+ border-bottom: 1px solid #ffffff;
+}
+
+
+
+.NEO .toolTipOff canvas,
+.NEO .toolTipOn canvas,
+.NEO .toolTipFixed canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.NEO .toolTipOff .label,
+.NEO .toolTipOn .label,
+.NEO .toolTipFixed .label {
+ position: absolute;
+ bottom: -4px;
+ left: 1px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: -1px;
+ overflow: hidden !important;
+}
+
+.NEO .colorTips {
+ position:relative;
+ width: 48px;
+ height: 144px;
+ padding: 0;
+ margin: 0;
+ margin-top: 4px;
+ margin-left: 0px;
+}
+
+.NEO .colorTipOff {
+ position: absolute;
+ overflow: hidden;
+ width: 22px;
+ height: 18px;
+ margin: -1px 4px 0px 0px;
+ padding: 0;
+}
+
+.NEO .colorTipOff img {
+ left: 0;
+ opacity: 0.5;
+ position: absolute;
+ pointer-events:none;
+}
+
+.NEO .colorTipOn {
+ position: absolute;
+ overflow: hidden;
+ width: 22px;
+ height: 18px;
+ margin: -1px 4px 0px 0px;
+ padding: 0;
+}
+
+.NEO .colorTipOn img {
+ left: -22px;
+ opacity: 0.5;
+ position: absolute;
+ pointer-events:none;
+}
+
+.NEO .colorSlider {
+ width: 48px;
+ height:13px;
+ position: relative;
+ margin-top: 3px;
+}
+
+.NEO .colorSlider .label {
+ pointer-events: none;
+ position: absolute;
+ left: 2px;
+ bottom: -3px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: middle;
+}
+
+.NEO .colorSlider .slider {
+ position: absolute;
+ height: 100%;
+ left: 0px;
+ width: 50%;
+ background-color: #fa9696;
+ box-shadow: -1px 0 0 0px rgba(0, 0, 0, 0.3) inset;
+}
+
+.NEO .colorSlider .hit {
+ position:absolute;
+ width: 60px;
+ height: 13px;
+ left: -6px;
+ background-color: white;
+ opacity: 0.01;
+}
+
+.NEO .sizeSlider {
+ width: 48px;
+ height:33px;
+ position: relative;
+ margin-top: 4px;
+}
+
+.NEO .sizeSlider .label {
+ pointer-events: none;
+ position: absolute;
+ left: 2px;
+ bottom: -3px;
+ font-size:12px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: middle;
+}
+
+.NEO .sizeSlider .slider {
+ position: absolute;
+ width: 100%;
+ top: 0px;
+ height: 33%;
+ background-color: #82f238;
+ box-shadow: 0 -1px 0 0px rgba(0, 0, 0, 0.3) inset;
+}
+
+.NEO .sizeSlider .hit {
+ position:absolute;
+ width: 48px;
+ height: 41px;
+ top: -4px;
+ background-color: white;
+ opacity: 0.01;
+}
+
+.NEO .reserveControl {
+ width: 48px;
+ height: 13px;
+ position: relative;
+}
+
+.NEO .reserveControl .reserve {
+ position: absolute;
+ width: 11px;
+ height: 8px;
+ background-color: white;
+}
+
+.NEO .layerControl {
+ width: 48px;
+ height: 20px;
+ position: relative;
+ margin-top: 6px;
+}
+
+.NEO .layerControl .bg {
+ position: absolute;
+ width: 44px;
+ height: 9px;
+ margin: 0;
+
+ top: 0px;
+ left: 2px;
+}
+
+.NEO .layerControl .line1 {
+ position: absolute;
+ width: 50px;
+ height:10px;
+ margin: 0;
+ padding: 0;
+
+ top: 0px;
+ left: -1px;
+ background-image: linear-gradient(to top right, transparent, transparent 47%, red 47%, red 53%, transparent 53%, transparent);
+}
+
+.NEO .layerControl .line0 {
+ position: absolute;
+ width: 50px;
+ height: 10px;
+ margin: 0;
+ padding: 0;
+
+ top: 10px;
+ left: -1px;
+ background-image: linear-gradient(to top right, transparent, transparent 47%, red 47%, red 53%, transparent 53%, transparent);
+}
+
+.NEO .layerControl .label1 {
+ position: absolute;
+ left: 2px;
+ Top: -4px;
+ font-size:11px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: baseline;
+}
+
+.NEO .layerControl .label0 {
+ position: absolute;
+ left: 2px;
+ Top: 6px;
+ font-size:11px;
+ font-weight: 100;
+ font-family: 'Arial';
+ letter-spacing: 0;
+ vertical-align: baseline;
+}
+
+/*
+-----------------------
+ InputText
+-----------------------
+*/
+
+.NEO .inputText {
+ z-index: 100000;
+ position: absolute;
+
+ text-align: left;
+ white-space: nowrap;
+ color: #0000ff;
+
+ font-family: 'Arial';
+ padding: 1px 5px 1px 5px;
+ font-size: 32px;
+
+ line-height: 40px;
+ height: 40px;
+ min-width: 10em;
+
+ bottom: 0px;
+ left: 250px;
+
+ border: 1px solid #aaa;
+ background-color: white;
+}
+
+*[contenteditable] {
+ user-select: auto !important;
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto !important;
+}
+
+#testtext {
+ user-select: auto !important;
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto;
+}
+
+[contenteditable="true"]:focus {
+ outline: 1px solid #b1d6fd;
+ border: 1px solid #b1d6fd;
+}
+
diff --git a/static/js/paintbbs/PaintBBS-1.3.4.js b/static/js/paintbbs/PaintBBS-1.3.4.js
new file mode 100644
index 0000000..8450a49
--- /dev/null
+++ b/static/js/paintbbs/PaintBBS-1.3.4.js
@@ -0,0 +1,6171 @@
+'use strict';
+
+document.addEventListener("DOMContentLoaded", function() {
+ Neo.init();
+
+ if (!navigator.userAgent.match("Electron")) {
+ Neo.start();
+ }
+});
+
+
+var Neo = function() {};
+
+Neo.version = "1.3.4";
+Neo.painter;
+Neo.fullScreen = false;
+Neo.uploaded = false;
+
+Neo.config = {
+ width: 300,
+ height: 300,
+
+ colors: [
+ "#000000", "#FFFFFF",
+ "#B47575", "#888888",
+ "#FA9696", "#C096C0",
+ "#FFB6FF", "#8080FF",
+ "#25C7C9", "#E7E58D",
+ "#E7962D", "#99CB7B",
+ "#FCECE2", "#F9DDCF"
+ ]
+};
+
+Neo.reservePen = {};
+Neo.reserveEraser = {};
+
+Neo.SLIDERTYPE_RED = 0;
+Neo.SLIDERTYPE_GREEN = 1;
+Neo.SLIDERTYPE_BLUE = 2;
+Neo.SLIDERTYPE_ALPHA = 3;
+Neo.SLIDERTYPE_SIZE = 4;
+
+document.neo = Neo;
+
+Neo.init = function() {
+ var applets = document.getElementsByTagName('applet');
+ if (applets.length == 0) {
+ applets = document.getElementsByTagName('applet-dummy');
+ }
+
+ for (var i = 0; i < applets.length; i++) {
+ var applet = applets[i];
+ var name = applet.attributes.name.value;
+ if (name == "paintbbs") {
+ Neo.applet = applet;
+ Neo.initConfig(applet);
+ Neo.createContainer(applet);
+ Neo.init2();
+ }
+ }
+};
+
+Neo.init2 = function() {
+ var pageview = document.getElementById("pageView");
+ pageview.style.width = Neo.config.applet_width + "px";
+ pageview.style.height = Neo.config.applet_height + "px";
+
+ Neo.canvas = document.getElementById("canvas");
+ Neo.container = document.getElementById("container");
+ Neo.toolsWrapper = document.getElementById("toolsWrapper");
+
+ Neo.painter = new Neo.Painter();
+ Neo.painter.build(Neo.canvas, Neo.config.width, Neo.config.height);
+
+ Neo.container.oncontextmenu = function() {return false;};
+
+ // 続ãã‹ã‚‰æã
+ if (Neo.config.image_canvas) {
+ Neo.painter.loadImage(Neo.config.image_canvas);
+ }
+
+ // æãã‹ã‘ã®ç”»åƒãŒè¦‹ã¤ã‹ã£ãŸã¨ã
+ Neo.storage = (Neo.isMobile()) ? localStorage : sessionStorage;
+ if (Neo.storage.getItem('timestamp')) {
+ setTimeout(function () {
+ if (confirm(Neo.translate("以å‰ã®ç·¨é›†ãƒ‡ãƒ¼ã‚¿ã‚’復元ã—ã¾ã™ã‹ï¼Ÿ"))) {
+ Neo.painter.loadSession();
+ }
+ }, 1);
+ }
+
+ window.addEventListener("pagehide", function(e) {
+ if (!Neo.uploaded) {
+ Neo.painter.saveSession();
+ } else {
+ Neo.painter.clearSession();
+ }
+ }, false);
+}
+
+Neo.initConfig = function(applet) {
+ if (applet) {
+ var name = applet.attributes.name.value || "neo";
+ var appletWidth = applet.attributes.width;
+ var appletHeight = applet.attributes.height;
+ if (appletWidth) Neo.config.applet_width = parseInt(appletWidth.value);
+ if (appletHeight) Neo.config.applet_height = parseInt(appletHeight.value);
+
+ var params = applet.getElementsByTagName('param');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i];
+ Neo.config[p.name] = Neo.fixConfig(p.value);
+
+ if (p.name == "image_width") Neo.config.width = parseInt(p.value);
+ if (p.name == "image_height") Neo.config.height = parseInt(p.value);
+ }
+
+ var emulationMode = Neo.config.neo_emulation_mode || "2.22_8x";
+ Neo.config.neo_alt_translation = emulationMode.slice(-1).match(/x/i);
+
+ Neo.readStyles();
+ Neo.applyStyle("color_bk", "#ccccff");
+ Neo.applyStyle("color_bk2", "#bbbbff");
+ Neo.applyStyle("color_tool_icon", "#e8dfae");
+ Neo.applyStyle("color_icon", "#ccccff");
+ Neo.applyStyle("color_iconselect", "#ffaaaa");
+ Neo.applyStyle("color_text", "#666699");
+ Neo.applyStyle("color_bar", "#6f6fae");
+ Neo.applyStyle("tool_color_button", "#e8dfae");
+ Neo.applyStyle("tool_color_button2", "#f8daaa");
+ Neo.applyStyle("tool_color_text", "#773333");
+ Neo.applyStyle("tool_color_bar", "#ddddff");
+ Neo.applyStyle("tool_color_frame", "#000000");
+
+ var e = document.getElementById("container");
+ Neo.config.inherit_color = Neo.getInheritColor(e);
+ if (!Neo.config.color_frame) Neo.config.color_frame = Neo.config.color_text;
+ }
+
+ Neo.config.reserves = [
+ { size:1,
+ color:"#000000", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_PEN,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ { size:5,
+ color:"#FFFFFF", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_ERASER,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ { size:10,
+ color:"#FFFFFF", alpha:1.0,
+ tool:Neo.Painter.TOOLTYPE_ERASER,
+ drawType:Neo.Painter.DRAWTYPE_FREEHAND
+ },
+ ];
+
+ Neo.reservePen = Neo.clone(Neo.config.reserves[0]);
+ Neo.reserveEraser = Neo.clone(Neo.config.reserves[1]);
+};
+
+Neo.fixConfig = function(value) {
+ // javaã§ã¯"#12345"を色ã¨ã—ã¦è§£é‡ˆã™ã‚‹ãŒjavascriptã§ã¯"#012345"ã«å¤‰æ›ã—ãªã„ã¨ã ã‚
+ if (value.match(/^#[0-9a-fA-F]{5}$/)) {
+ value = "#0" + value.slice(1);
+ }
+ return value;
+};
+
+Neo.initSkin = function() {
+ var sheet = document.styleSheets[0];
+ if (!sheet) {
+ var style = document.createElement("style");
+ document.head.appendChild(style); // must append before you can access sheet property
+ sheet = style.sheet;
+ }
+
+ Neo.styleSheet = sheet;
+
+ var lightBorder = Neo.multColor(Neo.config.color_icon, 1.3);
+ var darkBorder = Neo.multColor(Neo.config.color_icon, 0.7);
+ var lightBar = Neo.multColor(Neo.config.color_bar, 1.3);
+ var darkBar = Neo.multColor(Neo.config.color_bar, 0.7);
+ var bgImage = Neo.backgroundImage();
+
+ Neo.addRule(".NEO #container", "background-image", "url(" + bgImage + ")");
+ Neo.addRule(".NEO .colorSlider .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .sizeSlider .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .label1", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .label0", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .toolTipOn .label", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .toolTipOff .label", "color", Neo.config.tool_color_text);
+
+ Neo.addRule(".NEO #toolSet", "background-color", Neo.config.color_bk);
+ Neo.addRule(".NEO #tools", "color", Neo.config.tool_color_text);
+ Neo.addRule(".NEO .layerControl .bg", "border-bottom", "1px solid " + Neo.config.tool_color_text);
+
+ Neo.addRule(".NEO .buttonOn", "color", Neo.config.color_text);
+ Neo.addRule(".NEO .buttonOff", "color", Neo.config.color_text);
+
+ Neo.addRule(".NEO .buttonOff", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "border-top", "1px solid ", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "border-left", "1px solid ", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff", "box-shadow", "0 0 0 1px " + Neo.config.color_icon + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO .buttonOff:hover", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO .buttonOff:hover", "border-top", "1px solid " + lightBorder);
+ Neo.addRule(".NEO .buttonOff:hover", "border-left", "1px solid " + lightBorder);
+ Neo.addRule(".NEO .buttonOff:hover", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "background-color", darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "border-top", "1px solid " + darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "border-left", "1px solid " + darkBorder);
+ Neo.addRule(".NEO .buttonOff:active, .NEO .buttonOn", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect + ", 0 0 0 2px " + Neo.config.color_frame);
+
+
+ Neo.addRule(".NEO #canvas", "border", "1px solid " + Neo.config.color_frame);
+ Neo.addRule(".NEO #scrollH, .NEO #scrollV", "background-color", Neo.config.color_icon);
+ Neo.addRule(".NEO #scrollH, .NEO #scrollV", "box-shadow", "0 0 0 1px white" + ", 0 0 0 2px " + Neo.config.color_frame);
+
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "background-color", Neo.config.color_bar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "box-shadow", "0 0 0 1px " + Neo.config.color_icon);
+ Neo.addRule(".NEO #scrollH div:hover, .NEO #scrollV div:hover", "box-shadow", "0 0 0 1px " + Neo.config.color_iconselect);
+
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-top", "1px solid " + lightBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-left", "1px solid " + lightBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-right", "1px solid " + darkBar);
+ Neo.addRule(".NEO #scrollH div, .NEO #scrollV div", "border-bottom", "1px solid " + darkBar);
+
+ Neo.addRule(".NEO .toolTipOn", "background-color", Neo.multColor(Neo.config.tool_color_button, 0.7));
+ Neo.addRule(".NEO .toolTipOff", "background-color", Neo.config.tool_color_button);
+ Neo.addRule(".NEO .toolTipFixed", "background-color", Neo.config.tool_color_button2);
+
+ Neo.addRule(".NEO .colorSlider, .NEO .sizeSlider", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .reserveControl", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .reserveControl", "background-color", Neo.config.tool_color_bar);
+ Neo.addRule(".NEO .layerControl", "background-color", Neo.config.tool_color_bar);
+
+ Neo.addRule(".NEO .colorTipOn, .NEO .colorTipOff", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .toolTipOn, .NEO .toolTipOff", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .toolTipFixed", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .colorSlider, .NEO .sizeSlider", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .reserveControl", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .layerControl", "box-shadow", "0 0 0 1px " + Neo.config.tool_color_frame);
+ Neo.addRule(".NEO .reserveControl .reserve", "border", "1px solid " + Neo.config.tool_color_frame);
+
+ if (navigator.language.indexOf("ja") != 0) {
+ var labels = ["Fixed", "On", "Off"];
+ for (var i = 0; i < labels.length; i++) {
+ var selector = ".NEO .toolTip" + labels[i] + " .label";
+ Neo.addRule(selector, "letter-spacing", "0px !important");
+ }
+ }
+};
+
+Neo.addRule = function(selector, styleName, value, sheet) {
+ if (!sheet) sheet = Neo.styleSheet;
+ if (sheet.addRule) {
+ sheet.addRule(selector, styleName + ":" + value, sheet.rules.length);
+
+ } else if (sheet.insertRule) {
+ var rule = selector + "{" + styleName + ":" + value + "}";
+ var index = sheet.cssRules.length;
+ sheet.insertRule(rule, index);
+ }
+};
+
+Neo.readStyles = function() {
+ Neo.rules = {};
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ Neo.readStyle(document.styleSheets[i]);
+ }
+};
+
+Neo.readStyle = function(sheet) {
+ try {
+ var rules = sheet.cssRules;
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+ if (rule.styleSheet) {
+ Neo.readStyle(rule.styleSheet);
+ continue;
+ }
+
+ var selector = rule.selectorText
+ if (selector) {
+ selector = selector.replace(/^(.NEO\s+)?\./, '')
+
+ var css = rule.cssText || rule.style.cssText;
+ var result = css.match(/color:\s*(.*)\s*;/)
+ if (result) {
+ var hex = Neo.colorNameToHex(result[1]);
+ if (hex) {
+ Neo.rules[selector] = hex;
+ }
+ }
+ }
+ }
+ } catch (e) {}
+};
+
+Neo.applyStyle = function(name, defaultColor) {
+ if (Neo.config[name] == undefined) {
+ Neo.config[name] = Neo.rules[name] || defaultColor;
+ }
+};
+
+Neo.getInheritColor = function(e) {
+ var result = "#000000";
+ while (e && e.style) {
+ if (e.style.color != "") {
+ result = e.style.color;
+ break;
+ }
+ if (e.attributes["text"]) {
+ result = e.attributes["text"].value;
+ break;
+ }
+ e = e.parentNode;
+ }
+ return result;
+};
+
+Neo.backgroundImage = function() {
+ var c1 = Neo.painter.getColor(Neo.config.color_bk) | 0xff000000;
+ var c2 = Neo.painter.getColor(Neo.config.color_bk2) | 0xff000000;
+ var bgCanvas = document.createElement("canvas");
+ bgCanvas.width = 16;
+ bgCanvas.height = 16;
+ var ctx = bgCanvas.getContext("2d");
+ var imageData = ctx.getImageData(0, 0, 16, 16);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var index = 0;
+ for (var y = 0; y < 16; y++) {
+ for (var x = 0; x < 16; x++) {
+ buf32[index++] = (x == 14 || y == 14) ? c2 : c1;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+ return bgCanvas.toDataURL('image/png');
+};
+
+Neo.multColor = function(c, scale) {
+ var r = Math.round(parseInt(c.substr(1, 2), 16) * scale);
+ var g = Math.round(parseInt(c.substr(3, 2), 16) * scale);
+ var b = Math.round(parseInt(c.substr(5, 2), 16) * scale);
+ r = ("0" + Math.min(Math.max(r, 0), 255).toString(16)).substr(-2);
+ g = ("0" + Math.min(Math.max(g, 0), 255).toString(16)).substr(-2);
+ b = ("0" + Math.min(Math.max(b, 0), 255).toString(16)).substr(-2);
+ return '#' + r + g + b;
+};
+
+Neo.colorNameToHex = function(name) {
+ var colors = {"aliceblue":"#f0f8ff", "antiquewhite":"#faebd7", "aqua":"#00ffff","aquamarine":"#7fffd4", "azure":"#f0ffff", "beige":"#f5f5dc", "bisque":"#ffe4c4", "black":"#000000", "blanchedalmond":"#ffebcd", "blue":"#0000ff", "blueviolet":"#8a2be2", "brown":"#a52a2a", "burlywood":"#deb887", "cadetblue":"#5f9ea0", "chartreuse":"#7fff00", "chocolate":"#d2691e", "coral":"#ff7f50", "cornflowerblue":"#6495ed", "cornsilk":"#fff8dc", "crimson":"#dc143c", "cyan":"#00ffff", "darkblue":"#00008b", "darkcyan":"#008b8b", "darkgoldenrod":"#b8860b", "darkgray":"#a9a9a9", "darkgreen":"#006400", "darkkhaki":"#bdb76b", "darkmagenta":"#8b008b", "darkolivegreen":"#556b2f", "darkorange":"#ff8c00", "darkorchid":"#9932cc", "darkred":"#8b0000", "darksalmon":"#e9967a", "darkseagreen":"#8fbc8f", "darkslateblue":"#483d8b", "darkslategray":"#2f4f4f", "darkturquoise":"#00ced1", "darkviolet":"#9400d3", "deeppink":"#ff1493", "deepskyblue":"#00bfff", "dimgray":"#696969", "dodgerblue":"#1e90ff", "firebrick":"#b22222", "floralwhite":"#fffaf0", "forestgreen":"#228b22", "fuchsia":"#ff00ff", "gainsboro":"#dcdcdc", "ghostwhite":"#f8f8ff", "gold":"#ffd700", "goldenrod":"#daa520", "gray":"#808080", "green":"#008000", "greenyellow":"#adff2f", "honeydew":"#f0fff0", "hotpink":"#ff69b4", "indianred ":"#cd5c5c", "indigo":"#4b0082", "ivory":"#fffff0", "khaki":"#f0e68c", "lavender":"#e6e6fa", "lavenderblush":"#fff0f5", "lawngreen":"#7cfc00", "lemonchiffon":"#fffacd", "lightblue":"#add8e6", "lightcoral":"#f08080", "lightcyan":"#e0ffff", "lightgoldenrodyellow":"#fafad2", "lightgrey":"#d3d3d3", "lightgreen":"#90ee90", "lightpink":"#ffb6c1", "lightsalmon":"#ffa07a", "lightseagreen":"#20b2aa", "lightskyblue":"#87cefa", "lightslategray":"#778899", "lightsteelblue":"#b0c4de", "lightyellow":"#ffffe0", "lime":"#00ff00", "limegreen":"#32cd32", "linen":"#faf0e6", "magenta":"#ff00ff", "maroon":"#800000", "mediumaquamarine":"#66cdaa", "mediumblue":"#0000cd", "mediumorchid":"#ba55d3", "mediumpurple":"#9370d8", "mediumseagreen":"#3cb371", "mediumslateblue":"#7b68ee", "mediumspringgreen":"#00fa9a", "mediumturquoise":"#48d1cc", "mediumvioletred":"#c71585", "midnightblue":"#191970", "mintcream":"#f5fffa", "mistyrose":"#ffe4e1", "moccasin":"#ffe4b5", "navajowhite":"#ffdead", "navy":"#000080", "oldlace":"#fdf5e6", "olive":"#808000", "olivedrab":"#6b8e23", "orange":"#ffa500", "orangered":"#ff4500", "orchid":"#da70d6", "palegoldenrod":"#eee8aa", "palegreen":"#98fb98", "paleturquoise":"#afeeee", "palevioletred":"#d87093", "papayawhip":"#ffefd5", "peachpuff":"#ffdab9", "peru":"#cd853f", "pink":"#ffc0cb", "plum":"#dda0dd", "powderblue":"#b0e0e6", "purple":"#800080", "rebeccapurple":"#663399", "red":"#ff0000", "rosybrown":"#bc8f8f", "royalblue":"#4169e1", "saddlebrown":"#8b4513", "salmon":"#fa8072", "sandybrown":"#f4a460", "seagreen":"#2e8b57", "seashell":"#fff5ee", "sienna":"#a0522d", "silver":"#c0c0c0", "skyblue":"#87ceeb", "slateblue":"#6a5acd", "slategray":"#708090", "snow":"#fffafa", "springgreen":"#00ff7f", "steelblue":"#4682b4", "tan":"#d2b48c", "teal":"#008080", "thistle":"#d8bfd8", "tomato":"#ff6347", "turquoise":"#40e0d0", "violet":"#ee82ee", "wheat":"#f5deb3", "white":"#ffffff", "whitesmoke":"#f5f5f5", "yellow":"#ffff00", "yellowgreen":"#9acd32"};
+
+ var rgb = name.toLowerCase().match(/rgb\((.*),(.*),(.*)\)/);
+ if (rgb) {
+ var r = ("0" + parseInt(rgb[1]).toString(16)).slice(-2)
+ var g = ("0" + parseInt(rgb[2]).toString(16)).slice(-2)
+ var b = ("0" + parseInt(rgb[3]).toString(16)).slice(-2)
+ return "#" + r + g + b
+ }
+
+ if (typeof colors[name.toLowerCase()] != 'undefined') {
+ return colors[name.toLowerCase()];
+ }
+ return false;
+};
+
+Neo.initComponents = function() {
+ document.getElementById("copyright").innerHTML += "v" + Neo.version;
+
+ // アプレットã®borderã®å‹•ä½œã‚’エミュレート
+ if (navigator.userAgent.search("FireFox") > -1) {
+ var container = document.getElementById("container");
+ container.addEventListener("mousedown", function(e) {
+ container.style.borderColor = Neo.config.inherit_color;
+ e.stopPropagation();
+ }, false);
+ document.addEventListener("mousedown", function(e) {
+ container.style.borderColor = 'transparent';
+ }, false);
+ }
+
+ // ドラッグã—ãŸã¾ã¾ç”»é¢å¤–ã«ç§»å‹•ã—ãŸæ™‚
+ document.addEventListener("mouseup", function(e) {
+ if (Neo.painter && !Neo.painter.isContainer(e.target)) {
+ Neo.painter.cancelTool(e.target);
+ }
+ }, false);
+
+ // 投稿ã«å¤±æ•—ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã¨ãã¯è­¦å‘Šã‚’表示ã™ã‚‹
+ Neo.showWarning();
+
+ if (Neo.styleSheet) {
+ Neo.addRule("*", "user-select", "none");
+ Neo.addRule("*", "-webkit-user-select", "none");
+ Neo.addRule("*", "-ms-user-select", "none");
+ }
+}
+
+Neo.initButtons = function() {
+ new Neo.Button().init("undo").onmouseup = function() {
+ new Neo.UndoCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("redo").onmouseup = function () {
+ new Neo.RedoCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("window").onmouseup = function() {
+ new Neo.WindowCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("submit").onmouseup = function() {
+ new Neo.SubmitCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("copyright").onmouseup = function() {
+ new Neo.CopyrightCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("zoomPlus").onmouseup = function() {
+ new Neo.ZoomPlusCommand(Neo.painter).execute();
+ };
+ new Neo.Button().init("zoomMinus").onmouseup = function() {
+ new Neo.ZoomMinusCommand(Neo.painter).execute();
+ };
+
+ Neo.fillButton = new Neo.FillButton().init("fill");
+ Neo.rightButton = new Neo.RightButton().init("right");
+
+ if (Neo.isMobile()) {
+ Neo.rightButton.element.style.display = "block";
+ }
+
+ // toolTip
+ Neo.penTip = new Neo.PenTip().init("pen");
+ Neo.pen2Tip = new Neo.Pen2Tip().init("pen2");
+ Neo.effectTip = new Neo.EffectTip().init("effect");
+ Neo.effect2Tip = new Neo.Effect2Tip().init("effect2");
+ Neo.eraserTip = new Neo.EraserTip().init("eraser");
+ Neo.drawTip = new Neo.DrawTip().init("draw");
+ Neo.maskTip = new Neo.MaskTip().init("mask");
+
+ Neo.toolButtons = [Neo.fillButton,
+ Neo.penTip,
+ Neo.pen2Tip,
+ Neo.effectTip,
+ Neo.effect2Tip,
+ Neo.drawTip,
+ Neo.eraserTip];
+
+ // colorTip
+ for (var i = 1; i <= 14; i++) {
+ new Neo.ColorTip().init("color" + i, {index:i});
+ };
+
+ // colorSlider
+ Neo.sliders[Neo.SLIDERTYPE_RED] = new Neo.ColorSlider().init(
+ "sliderRed", {type:Neo.SLIDERTYPE_RED});
+ Neo.sliders[Neo.SLIDERTYPE_GREEN] = new Neo.ColorSlider().init(
+ "sliderGreen", {type:Neo.SLIDERTYPE_GREEN});
+ Neo.sliders[Neo.SLIDERTYPE_BLUE] = new Neo.ColorSlider().init(
+ "sliderBlue", {type:Neo.SLIDERTYPE_BLUE});
+ Neo.sliders[Neo.SLIDERTYPE_ALPHA] = new Neo.ColorSlider().init(
+ "sliderAlpha", {type:Neo.SLIDERTYPE_ALPHA});
+
+ // sizeSlider
+ Neo.sliders[Neo.SLIDERTYPE_SIZE] = new Neo.SizeSlider().init(
+ "sliderSize", {type:Neo.SLIDERTYPE_SIZE});
+
+ // reserveControl
+ for (var i = 1; i <= 3; i++) {
+ new Neo.ReserveControl().init("reserve" + i, {index:i});
+ };
+
+ new Neo.LayerControl().init("layerControl");
+ new Neo.ScrollBarButton().init("scrollH");
+ new Neo.ScrollBarButton().init("scrollV");
+};
+
+Neo.start = function(isApp) {
+ if (!Neo.painter) return;
+
+ Neo.initSkin();
+ Neo.initComponents();
+ Neo.initButtons();
+
+ Neo.isApp = isApp;
+ if (Neo.applet) {
+ var name = Neo.applet.attributes.name.value || "paintbbs";
+ Neo.applet.outerHTML = "";
+ document[name] = Neo;
+
+ Neo.resizeCanvas();
+ Neo.container.style.visibility = "visible";
+
+ if (Neo.isApp) {
+ var ipc = require('electron').ipcRenderer;
+ ipc.sendToHost('neo-status', 'ok');
+
+ } else {
+ if (document.paintBBSCallback) {
+ document.paintBBSCallback('start');
+ }
+ }
+ }
+};
+
+Neo.isIE = function() {
+ var ms = false;
+ if (/MSIE 10/i.test(navigator.userAgent)) {
+ ms = true; // This is internet explorer 10
+ }
+ if (/MSIE 9/i.test(navigator.userAgent) ||
+ /rv:11.0/i.test(navigator.userAgent)) {
+ ms = true; // This is internet explorer 9 or 11
+ }
+ return ms
+};
+
+Neo.isMobile = function() {
+ return navigator.userAgent.match(/Android|iPhone|iPad|iPod/i);
+};
+
+Neo.showWarning = function() {
+ var futaba = location.hostname.match(/2chan.net/i);
+ var samplebbs = location.hostname.match(/neo.websozai.jp/i);
+
+ var chrome = navigator.userAgent.match(/Chrome\/(\d+)/i);
+ if (chrome && chrome.length > 1) chrome = chrome[1];
+
+ var edge = navigator.userAgent.match(/Edge\/(\d+)/i);
+ if (edge && edge.length > 1) edge = edge[1];
+
+ var ms = Neo.isIE();
+
+ var str = "";
+ if (futaba || samplebbs) {
+ if (ms || (edge && edge < 15)) {
+ str = Neo.translate("ã“ã®ãƒ–ラウザã§ã¯<br>投稿ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™<br>");
+ }
+ }
+
+ // ã‚‚ã—<PARAM NAME="neo_warning" VALUE="...">ãŒã‚ã‚Œã°è¡¨ç¤ºã™ã‚‹
+ if (Neo.config.neo_warning) {
+ str += Neo.config.neo_warning;
+ }
+
+ var warning = document.getElementById("neoWarning")
+ warning.innerHTML = str;
+ setTimeout(function() { warning.style.opacity = "0"; }, 15000);
+};
+
+/*
+ -----------------------------------------------------------------------
+ UIã®æ›´æ–°
+ -----------------------------------------------------------------------
+*/
+
+Neo.updateUI = function() {
+ var current = Neo.painter.tool.getToolButton();
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ if (current) {
+ if (current == toolTip) {
+ toolTip.setSelected(true);
+ toolTip.update();
+ } else {
+ toolTip.setSelected(false);
+ }
+ }
+ }
+ if (Neo.drawTip) {
+ Neo.drawTip.update();
+ }
+
+ Neo.updateUIColor(true, false);
+}
+
+Neo.updateUIColor = function(updateSlider, updateColorTip) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.update();
+ }
+
+ if (updateSlider) {
+ for (var i = 0; i < Neo.sliders.length; i++) {
+ var slider = Neo.sliders[i];
+ slider.update();
+ }
+ }
+
+ // パレットを変更ã™ã‚‹ã¨ã
+ if (updateColorTip) {
+ var colorTip = Neo.ColorTip.getCurrent();
+ if (colorTip) {
+ colorTip.setColor(Neo.painter.foregroundColor);
+ }
+ }
+};
+
+/*
+ -----------------------------------------------------------------------
+ リサイズ対応
+ -----------------------------------------------------------------------
+*/
+
+Neo.updateWindow = function() {
+ if (Neo.fullScreen) {
+ document.getElementById("windowView").style.display = "block";
+ document.getElementById("windowView").appendChild(Neo.container);
+
+ } else {
+ document.getElementById("windowView").style.display = "none";
+ document.getElementById("pageView").appendChild(Neo.container);
+ }
+ Neo.resizeCanvas();
+};
+
+Neo.resizeCanvas = function() {
+ var appletWidth = Neo.container.clientWidth;
+ var appletHeight = Neo.container.clientHeight;
+
+ var canvasWidth = Neo.painter.canvasWidth;
+ var canvasHeight = Neo.painter.canvasHeight;
+
+ var width0 = canvasWidth * Neo.painter.zoom;
+ var height0 = canvasHeight * Neo.painter.zoom;
+
+ var width = (width0 < appletWidth - 100) ? width0 : appletWidth - 100;
+ var height = (height0 < appletHeight - 120) ? height0 : appletHeight - 120;
+
+ //width, heightã¯å¶æ•°ã§ãªã„ã¨èª¤å·®ãŒå‡ºã‚‹ãŸã‚
+ width = Math.floor(width / 2) * 2;
+ height = Math.floor(height / 2) * 2;
+
+ Neo.painter.destWidth = width;
+ Neo.painter.destHeight = height;
+
+ Neo.painter.destCanvas.width = width;
+ Neo.painter.destCanvas.height = height;
+ Neo.painter.destCanvasCtx = Neo.painter.destCanvas.getContext("2d");
+ Neo.painter.destCanvasCtx.imageSmoothingEnabled = false;
+ Neo.painter.destCanvasCtx.mozImageSmoothingEnabled = false;
+
+ Neo.canvas.style.width = width + "px";
+ Neo.canvas.style.height = height + "px";
+
+ var top = (Neo.container.clientHeight - toolsWrapper.clientHeight) / 2;
+ Neo.toolsWrapper.style.top = ((top > 0) ? top : 0) + "px";
+
+ if (top < 0) {
+ var s = Neo.container.clientHeight / toolsWrapper.clientHeight;
+ Neo.toolsWrapper.style.transform =
+ "translate(0, " + top + "px) scale(1," + s + ")";
+ } else {
+ Neo.toolsWrapper.style.transform = "";
+ }
+
+ Neo.painter.setZoom(Neo.painter.zoom);
+ Neo.painter.updateDestCanvas(0, 0, canvasWidth, canvasHeight);
+};
+
+/*
+ -----------------------------------------------------------------------
+ 投稿
+ -----------------------------------------------------------------------
+*/
+
+Neo.clone = function(src) {
+ var dst = {};
+ for (var k in src) {
+ dst[k] = src[k];
+ }
+ return dst;
+};
+
+Neo.getSizeString = function(len) {
+ var result = String(len);
+ while (result.length < 8) {
+ result = "0" + result;
+ }
+ return result;
+};
+
+Neo.openURL = function(url) {
+ if (Neo.isApp) {
+ require('electron').shell.openExternal(url);
+
+ } else {
+ window.open(url, '_blank');
+ }
+};
+
+Neo.submit = function(board, blob, thumbnail, thumbnail2) {
+ var url = Neo.config.url_save;
+ var headerString = Neo.str_header || "";
+ console.log("submit url=" + url + " header=" + headerString);
+
+ if (document.paintBBSCallback) {
+ var result = document.paintBBSCallback('check')
+ if (result == 0 || result == "false") {
+ return;
+ }
+
+ result = document.paintBBSCallback('header')
+ if (result && typeof result == "string") {
+ headerString == result;
+ }
+ }
+ if (!headerString) headerString = Neo.config.send_header || "";
+
+ var imageType = Neo.config.send_header_image_type;
+ if (imageType && imageType == "true") {
+ headerString = "image_type=png&" + headerString
+ console.log("header=" + headerString);
+ }
+
+ var header = new Blob([headerString]);
+ var headerLength = this.getSizeString(header.size);
+ var imgLength = this.getSizeString(blob.size);
+
+ var array = ['P', // PaintBBS
+ headerLength,
+ header,
+ imgLength,
+ '\r\n',
+ blob];
+
+ if (thumbnail) {
+ var thumbnailLength = this.getSizeString(thumbnail.size);
+ array.push(thumbnailLength, thumbnail);
+ }
+ if (thumbnail2) {
+ var thumbnail2Length = this.getSizeString(thumbnail2.size);
+ array.push(thumbnail2Length, thumbnail2);
+ }
+
+ var body = new Blob(array, {type: 'application/octet-binary'}); //ã“ã‚ŒãŒå¿…è¦ï¼ï¼
+
+ var request = new XMLHttpRequest();
+ request.open("POST", url, true);
+
+ request.onload = function(e) {
+ console.log(request.response);
+ Neo.uploaded = true;
+
+ /*var url = Neo.config.url_exit;
+ if (url[0] == '/') {
+ url = url.replace(/^.*\//, ''); //よãã‚ã‹ã‚“ãªã„ã‘ã©ã¨ã‚Šã‚ãˆãš
+ }
+
+ // ãµãŸã°ã®paintpost.phpã¯ã€ç”»åƒæŠ•ç¨¿ã«æˆåŠŸã™ã‚‹ã¨responseã«
+ // "./futaba.php?mode=paintcom&amp;painttmp=.png"
+ // ã¨ã„ã†æ–‡å­—列を返ã—ã¾ã™ã€‚
+ //
+ // NEOã§ã¯ã€responseã«æ–‡å­—列"painttmp="ãŒå«ã¾ã‚Œã‚‹å ´åˆã¯
+ // <PARAM>ã§æŒ‡å®šã•ã‚ŒãŸurl_exitを無視ã—ã¦ã€ã“ã®URLã«ã‚¸ãƒ£ãƒ³ãƒ—ã—ã¾ã™ã€‚
+ var responseURL = request.response.replace(/&amp;/g, '&');
+ if (responseURL.match(/painttmp=/)) {
+ url = responseURL;
+ }
+ var exitURL = url;
+
+ // ã—ãƒã¡ã‚ƒã‚“ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’よã見ãŸã‚‰
+ // response㌠"URL:〜" ã®å½¢ã ã£ãŸå ´åˆã¯ãã“ã¸é£›ã°ã™ã£ã¦æ›¸ã„ã¦ã‚ã‚Šã¾ã—ãŸã€‚
+ // ã“ã£ã¡ã‚’使ã†ã¹ãã§ã—ãŸâ€¦â€¦
+ if (responseURL.match(/^URL:/)) {
+ exitURL = responseURL.replace(/^URL:/, '');
+ }
+
+ location.href = exitURL;*/
+ location.href = Neo.config.url_exit;
+ };
+ request.onerror = function(e) {
+ console.log("error");
+ };
+ request.onabort = function(e) {
+ console.log("abort");
+ };
+ request.ontimeout = function(e) {
+ console.log("timeout");
+ };
+
+ request.send(body);
+};
+
+/*
+ -----------------------------------------------------------------------
+ LiveConnect
+ -----------------------------------------------------------------------
+*/
+
+Neo.getColors = function() {
+ console.log("getColors")
+ console.log("defaultColors==", Neo.config.colors.join('\n'));
+ var array = []
+ for (var i = 0; i < 14; i++) {
+ array.push(Neo.colorTips[i].color)
+ }
+ return array.join('\n');
+ // return Neo.config.colors.join('\n');
+};
+
+Neo.setColors = function(colors) {
+ console.log("setColors");
+ var array = colors.split('\n');
+ for (var i = 0; i < 14; i++) {
+ var color = array[i];
+ Neo.config.colors[i] = color;
+ Neo.colorTips[i].setColor(color);
+ }
+};
+
+
+Neo.pExit = function() {
+ new Neo.SubmitCommand(Neo.painter).execute();
+};
+
+Neo.str_header = "";
+
+/*
+ -----------------------------------------------------------------------
+ DOMツリーã®ä½œæˆ
+ -----------------------------------------------------------------------
+*/
+
+Neo.createContainer = function(applet) {
+ var neo = document.createElement("div");
+ neo.className = "NEO";
+ neo.id = "NEO";
+ var html = (function() {/*
+
+<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
+
+<div id="pageView" style="width:450px; height:470px; margin:auto;">
+<div id="container" style="visibility:hidden;" class="o">
+<div id="center" class="o">
+<div id="painterContainer" class="o">
+<div id="painterWrapper" class="o">
+<div id="upper" class="o">
+<div id="redo">[ã‚„ã‚Šç›´ã—]</div>
+<div id="undo">[å…ƒã«æˆ»ã™]</div>
+<div id="fill">[å¡—ã‚Šæ½°ã—]</div>
+<div id="right" style="display:none;">[å³]</div>
+</div>
+<div id="painter">
+<div id="canvas"> <!-- class="o">-->
+<div id="scrollH"></div>
+<div id="scrollV"></div>
+<div id="zoomPlusWrapper">
+<div id="zoomPlus">+</div>
+</div>
+<div id="zoomMinusWrapper">
+<div id="zoomMinus">-</div>
+</div>
+<div id="neoWarning"></div>
+</div>
+</div>
+<div id="lower" class="o">
+</div>
+</div>
+<div id="toolsWrapper">
+<div id="tools">
+<div id="toolSet">
+<div id="pen"></div>
+<div id="pen2"></div>
+<div id="effect"></div>
+<div id="effect2"></div>
+<div id="eraser"></div>
+<div id="draw"></div>
+<div id="mask"></div>
+
+<div class="colorTips">
+<div id="color2"></div><div id="color1"></div><br>
+<div id="color4"></div><div id="color3"></div><br>
+<div id="color6"></div><div id="color5"></div><br>
+<div id="color8"></div><div id="color7"></div><br>
+<div id="color10"></div><div id="color9"></div><br>
+<div id="color12"></div><div id="color11"></div><br>
+<div id="color14"></div><div id="color13"></div>
+</div>
+
+<div id="sliderRed"></div>
+<div id="sliderGreen"></div>
+<div id="sliderBlue"></div>
+<div id="sliderAlpha"></div>
+<div id="sliderSize"></div>
+
+<div class="reserveControl" style="margin-top:4px;">
+<div id="reserve1"></div>
+<div id="reserve2"></div>
+<div id="reserve3"></div>
+</div>
+<div id="layerControl" style="margin-top:6px;"></div>
+
+<!--<div id="toolPad" style="height:20px;"></div>-->
+</div>
+</div>
+</div>
+</div>
+</div>
+<div id="headerButtons">
+<div id="window">[窓]</div>
+</div>
+<div id="footerButtons">
+<div id="submit">[投稿]</div>
+<div id="copyright">[(C)ã—ãƒã¡ã‚ƒã‚“ PaintBBS NEO]</div>
+</div>
+</div>
+</div>
+
+<div id="windowView" style="display: none;">
+
+</div>
+
+
+ */}).toString().match(/\/\*([^]*)\*\//)[1];
+
+ neo.innerHTML = html.replace(/\[(.*?)\]/g, function(match, str) {
+ return Neo.translate(str)
+ })
+
+ var parent = applet.parentNode;
+ parent.appendChild(neo);
+ parent.insertBefore(neo, applet);
+
+ // applet.style.display = "none";
+
+ // NEOを組ã¿è¾¼ã‚“ã URLをアプリ版ã§é–‹ãã¨DOMツリーãŒ2é‡ã«ã§ãã¦æ ¼å¥½æ‚ªã„ã®ã§æ¶ˆã—ã¦ãŠã
+ setTimeout(function() {
+ var tmp = document.getElementsByClassName("NEO");
+ if (tmp.length > 1) {
+ for (var i = 1; i < tmp.length; i++) {
+ tmp[i].style.display = "none";
+ }
+ }
+ }, 0);
+};
+
+
+'use strict';
+
+Neo.dictionary = {
+ "ja": {},
+ "en": {
+ "ã‚„ã‚Šç›´ã—": "Redo",
+ "å…ƒã«æˆ»ã™": "Undo",
+ "å¡—ã‚Šæ½°ã—": "Paint",
+ "窓": "F&nbsp;",
+ "投稿": "Send",
+ "(C)ã—ãƒã¡ã‚ƒã‚“ PaintBBS NEO": "(C)shi-chan PaintBBS NEO",
+ "鉛筆": "Solid",
+ "水彩": "WaterC",
+ "テキスト": "Text",
+ "トーン": "Tone",
+ "ã¼ã‹ã—": "ShadeOff",
+ "覆ã„焼ã": "HLight",
+ "焼ãè¾¼ã¿": "Dark",
+ "消ã—ペン": "White",
+ "消ã—四角": "WhiteRect",
+ "全消ã—": "Clear",
+ "四角": "Rect",
+ "線四角": "LineRect",
+ "楕円": "Oval",
+ "線楕円": "LineOval",
+ "コピー": "Copy",
+ "レイヤçµåˆ": "lay-unif",
+ "角å–ã‚Š": "Antialias",
+ "å·¦å³å転": "reverseL",
+ "上下å転": "reverseU",
+ "傾ã‘": "lie",
+ "通常": "Normal",
+ "マスク": "Mask",
+ "逆ï¾ï½½ï½¸": "ReMask",
+ "加算": "And",
+ "逆加算": "Div",
+ "手書ã": "FreeLine",
+ "ç›´ç·š": "Straight",
+ "BZ曲線": "Bezie",
+ "ページビュー?": "Page view?",
+ "ウィンドウビュー?": "Window view?",
+ "以å‰ã®ç·¨é›†ãƒ‡ãƒ¼ã‚¿ã‚’復元ã—ã¾ã™ã‹ï¼Ÿ": "Restore session?",
+ "å³": "Right Click",
+
+ "PaintBBS NEOã¯ã€ãŠçµµæãã—ãƒæŽ²ç¤ºæ¿ PaintBBS (©2000-2004 ã—ãƒã¡ã‚ƒã‚“) ã‚’html5化ã™ã‚‹ãƒ—ロジェクトã§ã™ã€‚\n\nPaintBBS NEOã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ": "PaintBBS NEO is an HTML5 port of Oekaki Shi-BBS PaintBBS (©2000-2004 shi-chan). Show the project page?",
+ "ã“ã®ãƒ–ラウザã§ã¯<br>投稿ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™<br>": "This browser may fail to send your picture.<br>",
+ },
+ "enx": {
+ "ã‚„ã‚Šç›´ã—": "Redo",
+ "å…ƒã«æˆ»ã™": "Undo",
+ "å¡—ã‚Šæ½°ã—": "Fill",
+ "窓": "Float",
+ "投稿": "Send",
+ "(C)ã—ãƒã¡ã‚ƒã‚“ PaintBBS NEO": "&copy;shi-cyan PaintBBS NEO",
+ "鉛筆": "Solid",
+ "水彩": "WaterCo",
+ "テキスト": "Text",
+ "トーン": "Halftone",
+ "ã¼ã‹ã—": "Blur",
+ "覆ã„焼ã": "Light",
+ "焼ãè¾¼ã¿": "Dark",
+ "消ã—ペン": "White",
+ "消ã—四角": "WhiteRe",
+ "全消ã—": "Clear",
+ "四角": "Rect",
+ "線四角": "LineRect",
+ "楕円": "Oval",
+ "線楕円": "LineOval",
+ "コピー": "Copy",
+ "レイヤçµåˆ": "layerUnit",
+ "角å–ã‚Š": "antiAlias",
+ "å·¦å³å転": "flipHorita",
+ "上下å転": "flipVertic",
+ "傾ã‘": "rotate",
+ "通常": "Normal",
+ "マスク": "Mask",
+ "逆ï¾ï½½ï½¸": "ReMask",
+ "加算": "And",
+ "逆加算": "Divide",
+ "手書ã": "Freehan",
+ "ç›´ç·š": "Line",
+ "BZ曲線": "Bezier",
+ "Layer0": "LayerBG",
+ "Layer1": "LayerFG",
+ "ページビュー?": "Page view?",
+ "ウィンドウビュー?": "Window view?",
+ "以å‰ã®ç·¨é›†ãƒ‡ãƒ¼ã‚¿ã‚’復元ã—ã¾ã™ã‹ï¼Ÿ": "Restore session?",
+ "å³": "Right Click",
+
+ "PaintBBS NEOã¯ã€ãŠçµµæãã—ãƒæŽ²ç¤ºæ¿ PaintBBS (©2000-2004 ã—ãƒã¡ã‚ƒã‚“) ã‚’html5化ã™ã‚‹ãƒ—ロジェクトã§ã™ã€‚\n\nPaintBBS NEOã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ": "PaintBBS NEO is an HTML5 port of Oekaki Shi-BBS PaintBBS (©2000-2004 shi-chan). Show the project page?",
+ "ã“ã®ãƒ–ラウザã§ã¯<br>投稿ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™<br>": "This browser may fail to send your picture.<br>",
+ },
+ "es": {
+ "ã‚„ã‚Šç›´ã—": "Rehacer",
+ "å…ƒã«æˆ»ã™": "Deshacer",
+ "å¡—ã‚Šæ½°ã—": "Llenar",
+ "窓": "Ventana",
+ "投稿": "Enviar",
+ "(C)ã—ãƒã¡ã‚ƒã‚“ PaintBBS NEO": "&copy;shi-cyan PaintBBS NEO",
+ "鉛筆": "Lápiz",
+ "水彩": "Acuarela",
+ "テキスト": "Texto",
+ "トーン": "Tono",
+ "ã¼ã‹ã—": "Gradación",
+ "覆ã„焼ã": "Sobreexp.",
+ "焼ãè¾¼ã¿": "Quemar",
+ "消ã—ペン": "Goma",
+ "消ã—四角": "GomaRect",
+ "全消ã—": "Borrar",
+ "四角": "Rect",
+ "線四角": "LíneaRect",
+ "楕円": "Óvalo",
+ "線楕円": "LíneaÓvalo",
+ "コピー": "Copiar",
+ "レイヤçµåˆ": "UnirCapa",
+ "角å–ã‚Š": "Antialias",
+ "å·¦å³å転": "Inv.Izq/Der",
+ "上下å転": "Inv.Arr/Aba",
+ "傾ã‘": "Inclinar",
+ "通常": "Normal",
+ "マスク": "Masc.",
+ "逆ï¾ï½½ï½¸": "Masc.Inv",
+ "加算": "Adición",
+ "逆加算": "Subtrac",
+ "手書ã": "Libre",
+ "直線": "Línea",
+ "BZ曲線": "Curva",
+ "Layer0": "Capa0",
+ "Layer1": "Capa1",
+ "ページビュー?": "¿Vista de página?",
+ "ウィンドウビュー?": "¿Vista de ventana?",
+ "以å‰ã®ç·¨é›†ãƒ‡ãƒ¼ã‚¿ã‚’復元ã—ã¾ã™ã‹ï¼Ÿ": "¿Restaurar sesión anterior?",
+ "å³": "Clic derecho",
+
+ "PaintBBS NEOã¯ã€ãŠçµµæãã—ãƒæŽ²ç¤ºæ¿ PaintBBS (©2000-2004 ã—ãƒã¡ã‚ƒã‚“) ã‚’html5化ã™ã‚‹ãƒ—ロジェクトã§ã™ã€‚\n\nPaintBBS NEOã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ":
+ "PaintBBS NEO es una versión para HTML5 de Oekaki Shi-BBS PaintBBS (© 2000-2004 shi-chan). ¿Mostrar la página del proyecto?",
+ "ã“ã®ãƒ–ラウザã§ã¯<br>投稿ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™<br>": "Este navegador podría no enviar su imagen.<br>",
+ },
+};
+
+Neo.translate = function () {
+ var language = (window.navigator.languages && window.navigator.languages[0]) ||
+ window.navigator.language ||
+ window.navigator.userLanguage ||
+ window.navigator.browserLanguage;
+
+ var lang = "en";
+ for (var key in Neo.dictionary) {
+ if (language.indexOf(key) == 0) {
+ lang = key;
+ break;
+ }
+ }
+
+ return function(string) {
+ if (Neo.config.neo_alt_translation) {
+ if (lang == "en") lang = "enx"
+ } else {
+ if (lang != "ja") lang = "en"
+ }
+ return Neo.dictionary[lang][string] || string;
+ }
+}();
+
+
+'use strict';
+
+Neo.Painter = function() {
+ this._undoMgr = new Neo.UndoManager(50);
+};
+
+Neo.Painter.prototype.container;
+Neo.Painter.prototype._undoMgr;
+Neo.Painter.prototype.tool;
+Neo.Painter.prototype.inputText;
+
+//Canvas Info
+Neo.Painter.prototype.canvasWidth;
+Neo.Painter.prototype.canvasHeight;
+Neo.Painter.prototype.canvas = [];
+Neo.Painter.prototype.canvasCtx = [];
+Neo.Painter.prototype.visible = [];
+Neo.Painter.prototype.current = 0;
+
+//Temp Canvas Info
+Neo.Painter.prototype.tempCanvas;
+Neo.Painter.prototype.tempCanvasCtx;
+Neo.Painter.prototype.tempX = 0;
+Neo.Painter.prototype.tempY = 0;
+
+//Destination Canvas for display
+Neo.Painter.prototype.destCanvas;
+Neo.Painter.prototype.destCanvasCtx;
+
+
+Neo.Painter.prototype.backgroundColor = "#ffffff";
+Neo.Painter.prototype.foregroundColor = "#000000";
+
+Neo.Painter.prototype.lineWidth = 1;
+Neo.Painter.prototype.alpha = 1;
+Neo.Painter.prototype.zoom = 1;
+Neo.Painter.prototype.zoomX = 0;
+Neo.Painter.prototype.zoomY = 0;
+
+Neo.Painter.prototype.isMouseDown;
+Neo.Painter.prototype.isMouseDownRight;
+Neo.Painter.prototype.prevMouseX;
+Neo.Painter.prototype.prevMouseY;
+Neo.Painter.prototype.mouseX;
+Neo.Painter.prototype.mouseY;
+
+Neo.Painter.prototype.slowX = 0;
+Neo.Painter.prototype.slowY = 0;
+Neo.Painter.prototype.stab = null;
+
+Neo.Painter.prototype.isShiftDown = false;
+Neo.Painter.prototype.isCtrlDown = false;
+Neo.Painter.prototype.isAltDown = false;
+
+//Neo.Painter.prototype.touchModifier = null;
+Neo.Painter.prototype.virtualRight = false;
+Neo.Painter.prototype.virtualShift = false;
+
+//Neo.Painter.prototype.onUpdateCanvas;
+Neo.Painter.prototype._roundData = [];
+Neo.Painter.prototype._toneData = [];
+Neo.Painter.prototype.toolStack = [];
+
+Neo.Painter.prototype.maskType = 0;
+Neo.Painter.prototype.drawType = 0;
+Neo.Painter.prototype.maskColor = "#000000";
+Neo.Painter.prototype._currentColor = [];
+Neo.Painter.prototype._currentMask = [];
+
+Neo.Painter.prototype.aerr;
+
+Neo.Painter.LINETYPE_NONE = 0;
+Neo.Painter.LINETYPE_PEN = 1;
+Neo.Painter.LINETYPE_ERASER = 2;
+Neo.Painter.LINETYPE_BRUSH = 3;
+Neo.Painter.LINETYPE_TONE = 4;
+Neo.Painter.LINETYPE_DODGE = 5;
+Neo.Painter.LINETYPE_BURN = 6;
+
+Neo.Painter.MASKTYPE_NONE = 0;
+Neo.Painter.MASKTYPE_NORMAL = 1;
+Neo.Painter.MASKTYPE_REVERSE = 2;
+Neo.Painter.MASKTYPE_ADD = 3;
+Neo.Painter.MASKTYPE_SUB = 4;
+
+Neo.Painter.DRAWTYPE_FREEHAND = 0;
+Neo.Painter.DRAWTYPE_LINE = 1;
+Neo.Painter.DRAWTYPE_BEZIER = 2;
+
+Neo.Painter.ALPHATYPE_NONE = 0;
+Neo.Painter.ALPHATYPE_PEN = 1;
+Neo.Painter.ALPHATYPE_FILL = 2;
+Neo.Painter.ALPHATYPE_BRUSH = 3;
+
+Neo.Painter.TOOLTYPE_NONE = 0;
+Neo.Painter.TOOLTYPE_PEN = 1;
+Neo.Painter.TOOLTYPE_ERASER = 2;
+Neo.Painter.TOOLTYPE_HAND = 3;
+Neo.Painter.TOOLTYPE_SLIDER = 4;
+Neo.Painter.TOOLTYPE_FILL = 5;
+Neo.Painter.TOOLTYPE_MASK = 6;
+Neo.Painter.TOOLTYPE_ERASEALL = 7;
+Neo.Painter.TOOLTYPE_ERASERECT = 8;
+Neo.Painter.TOOLTYPE_COPY = 9;
+Neo.Painter.TOOLTYPE_PASTE = 10;
+Neo.Painter.TOOLTYPE_MERGE = 11;
+Neo.Painter.TOOLTYPE_FLIP_H = 12;
+Neo.Painter.TOOLTYPE_FLIP_V = 13;
+
+Neo.Painter.TOOLTYPE_BRUSH = 14;
+Neo.Painter.TOOLTYPE_TEXT = 15;
+Neo.Painter.TOOLTYPE_TONE = 16;
+Neo.Painter.TOOLTYPE_BLUR = 17;
+Neo.Painter.TOOLTYPE_DODGE = 18;
+Neo.Painter.TOOLTYPE_BURN = 19;
+Neo.Painter.TOOLTYPE_RECT = 20;
+Neo.Painter.TOOLTYPE_RECTFILL = 21;
+Neo.Painter.TOOLTYPE_ELLIPSE = 22;
+Neo.Painter.TOOLTYPE_ELLIPSEFILL = 23;
+Neo.Painter.TOOLTYPE_BLURRECT = 24;
+Neo.Painter.TOOLTYPE_TURN = 25;
+
+Neo.Painter.prototype.build = function(div, width, height)
+{
+ this.container = div;
+ this._initCanvas(div, width, height);
+ this._initRoundData();
+ this._initToneData();
+ this._initInputText();
+
+ this.setTool(new Neo.PenTool());
+
+};
+
+Neo.Painter.prototype.setTool = function(tool) {
+ if (this.tool && this.tool.saveStates) this.tool.saveStates();
+
+ if (this.tool && this.tool.kill) {
+ this.tool.kill();
+ }
+ this.tool = tool;
+ tool.init();
+ if (this.tool && this.tool.loadStates) this.tool.loadStates();
+};
+
+Neo.Painter.prototype.pushTool = function(tool) {
+ this.toolStack.push(this.tool);
+ this.tool = tool;
+ tool.init();
+};
+
+Neo.Painter.prototype.popTool = function() {
+ var tool = this.tool;
+ if (tool && tool.kill) {
+ tool.kill();
+ }
+ this.tool = this.toolStack.pop();
+};
+
+Neo.Painter.prototype.getCurrentTool = function() {
+ if (this.tool) {
+ var tool = this.tool;
+ if (tool && tool.type == Neo.Painter.TOOLTYPE_SLIDER) {
+ var stack = this.toolStack;
+ if (stack.length > 0) {
+ tool = stack[stack.length - 1];
+ }
+ }
+ return tool;
+ }
+ return null;
+};
+
+Neo.Painter.prototype.setToolByType = function(toolType) {
+ switch (parseInt(toolType)) {
+ case Neo.Painter.TOOLTYPE_PEN: this.setTool(new Neo.PenTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASER: this.setTool(new Neo.EraserTool()); break;
+ case Neo.Painter.TOOLTYPE_HAND: this.setTool(new Neo.HandTool()); break;
+ case Neo.Painter.TOOLTYPE_FILL: this.setTool(new Neo.FillTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASEALL: this.setTool(new Neo.EraseAllTool()); break;
+ case Neo.Painter.TOOLTYPE_ERASERECT: this.setTool(new Neo.EraseRectTool()); break;
+
+ case Neo.Painter.TOOLTYPE_COPY: this.setTool(new Neo.CopyTool()); break;
+ case Neo.Painter.TOOLTYPE_PASTE: this.setTool(new Neo.PasteTool()); break;
+ case Neo.Painter.TOOLTYPE_MERGE: this.setTool(new Neo.MergeTool()); break;
+ case Neo.Painter.TOOLTYPE_FLIP_H: this.setTool(new Neo.FlipHTool()); break;
+ case Neo.Painter.TOOLTYPE_FLIP_V: this.setTool(new Neo.FlipVTool()); break;
+
+ case Neo.Painter.TOOLTYPE_BRUSH: this.setTool(new Neo.BrushTool()); break;
+ case Neo.Painter.TOOLTYPE_TEXT: this.setTool(new Neo.TextTool()); break;
+ case Neo.Painter.TOOLTYPE_TONE: this.setTool(new Neo.ToneTool()); break;
+ case Neo.Painter.TOOLTYPE_BLUR: this.setTool(new Neo.BlurTool()); break;
+ case Neo.Painter.TOOLTYPE_DODGE: this.setTool(new Neo.DodgeTool()); break;
+ case Neo.Painter.TOOLTYPE_BURN: this.setTool(new Neo.BurnTool()); break;
+
+ case Neo.Painter.TOOLTYPE_RECT: this.setTool(new Neo.RectTool()); break;
+ case Neo.Painter.TOOLTYPE_RECTFILL: this.setTool(new Neo.RectFillTool()); break;
+ case Neo.Painter.TOOLTYPE_ELLIPSE: this.setTool(new Neo.EllipseTool()); break;
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:this.setTool(new Neo.EllipseFillTool()); break;
+ case Neo.Painter.TOOLTYPE_BLURRECT: this.setTool(new Neo.BlurRectTool()); break;
+ case Neo.Painter.TOOLTYPE_TURN: this.setTool(new Neo.TurnTool()); break;
+
+ default:
+ console.log("unknown toolType " + toolType);
+ break;
+ }
+};
+
+Neo.Painter.prototype._initCanvas = function(div, width, height) {
+ width = parseInt(width);
+ height = parseInt(height);
+ var destWidth = parseInt(div.clientWidth);
+ var destHeight = parseInt(div.clientHeight);
+ this.destWidth = width;
+ this.destHeight = height;
+
+ this.canvasWidth = width;
+ this.canvasHeight = height;
+ this.zoomX = width * 0.5;
+ this.zoomY = height * 0.5;
+
+ for (var i = 0; i < 2; i++) {
+ this.canvas[i] = document.createElement("canvas");
+ this.canvas[i].width = width;
+ this.canvas[i].height = height;
+ this.canvasCtx[i] = this.canvas[i].getContext("2d");
+
+ this.canvas[i].style.imageRendering = "pixelated";
+ this.canvasCtx[i].imageSmoothingEnabled = false;
+ this.canvasCtx[i].mozImageSmoothingEnabled = false;
+ this.visible[i] = true;
+ }
+
+ this.tempCanvas = document.createElement("canvas");
+ this.tempCanvas.width = width;
+ this.tempCanvas.height = height;
+ this.tempCanvasCtx = this.tempCanvas.getContext("2d");
+ this.tempCanvas.style.position = "absolute";
+ this.tempCanvas.enabled = false;
+
+ var array = this.container.getElementsByTagName("canvas");
+ if (array.length > 0) {
+ this.destCanvas = array[0];
+ } else {
+ this.destCanvas = document.createElement("canvas");
+ this.container.appendChild(this.destCanvas);
+ }
+
+ this.destCanvasCtx = this.destCanvas.getContext("2d");
+ this.destCanvas.width = destWidth;
+ this.destCanvas.height = destHeight;
+
+ this.destCanvas.style.imageRendering = "pixelated";
+ this.destCanvasCtx.imageSmoothingEnabled = false;
+ this.destCanvasCtx.mozImageSmoothingEnabled = false;
+
+ var ref = this;
+
+ var container = document.getElementById("container");
+
+ container.onmousedown = function(e) {ref._mouseDownHandler(e)};
+ container.onmousemove = function(e) {ref._mouseMoveHandler(e)};
+ container.onmouseup = function(e) {ref._mouseUpHandler(e)};
+ container.onmouseover = function(e) {ref._rollOverHandler(e)};
+ container.onmouseout = function(e) {ref._rollOutHandler(e)};
+ container.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ }, false);
+ container.addEventListener("touchmove", function(e) {
+ ref._mouseMoveHandler(e);
+ }, false);
+ container.addEventListener("touchend", function(e) {
+ ref._mouseUpHandler(e);
+ }, false);
+
+ document.onkeydown = function(e) {ref._keyDownHandler(e)};
+ document.onkeyup = function(e) {ref._keyUpHandler(e)};
+
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype._initRoundData = function() {
+ for (var r = 1; r <= 30; r++) {
+ this._roundData[r] = new Uint8Array(r * r);
+ var mask = this._roundData[r];
+ var d = Math.floor(r / 2.0);
+ var index = 0;
+ for (var x = 0; x < r; x++) {
+ for (var y = 0; y < r; y++) {
+ var xx = x + 0.5 - r/2.0;
+ var yy = y + 0.5 - r/2.0;
+ mask[index++] = (xx*xx + yy*yy <= r*r/4) ? 1 : 0;
+ }
+ }
+ }
+ this._roundData[3][0] = 0;
+ this._roundData[3][2] = 0;
+ this._roundData[3][6] = 0;
+ this._roundData[3][8] = 0;
+
+ this._roundData[5][1] = 0;
+ this._roundData[5][3] = 0;
+ this._roundData[5][5] = 0;
+ this._roundData[5][9] = 0;
+ this._roundData[5][15] = 0;
+ this._roundData[5][19] = 0;
+ this._roundData[5][21] = 0;
+ this._roundData[5][23] = 0;
+};
+
+Neo.Painter.prototype._initToneData = function() {
+ var pattern = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
+
+ for (var i = 0; i < 16; i++) {
+ this._toneData[i] = new Uint8Array(16);
+ for (var j = 0; j < 16; j++) {
+ this._toneData[i][j] = (i >= pattern[j]) ? 1 : 0;
+ }
+ }
+};
+
+Neo.Painter.prototype.getToneData = function(alpha) {
+ var alphaTable = [23,
+ 47,
+ 69,
+ 92,
+ 114,
+ 114,
+ 114,
+ 138,
+ 161,
+ 184,
+ 184,
+ 207,
+ 230,
+ 230,
+ 253,
+ ];
+
+ for (var i = 0; i < alphaTable.length; i++) {
+ if (alpha < alphaTable[i]) {
+ return this._toneData[i];
+ }
+ }
+ return this._toneData[i];
+};
+
+Neo.Painter.prototype._initInputText = function() {
+ var text = document.getElementById("inputtext");
+ if (!text) {
+ text = document.createElement("div");
+ }
+
+ text.id = "inputext";
+ text.setAttribute("contentEditable", true);
+ text.spellcheck = false;
+ text.className = "inputText";
+ text.innerHTML = "";
+
+ text.style.display = "none";
+// text.style.userSelect = "none";
+ Neo.painter.container.appendChild(text);
+ this.inputText = text;
+
+ this.updateInputText();
+};
+
+Neo.Painter.prototype.hideInputText = function() {
+ var text = this.inputText;
+ text.blur();
+ text.style.display = "none";
+};
+
+Neo.Painter.prototype.updateInputText = function() {
+ var text = this.inputText;
+ var d = this.lineWidth;
+ var fontSize = Math.round(d * 55/28 + 7);
+ var height = Math.round(d * 68/28 + 12);
+
+ text.style.fontSize = fontSize + "px";
+ text.style.lineHeight = fontSize + "px";
+ text.style.height = fontSize + "px";
+ text.style.marginTop = -fontSize + "px";
+};
+
+/*
+-----------------------------------------------------------------------
+ Mouse Event Handling
+-----------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype._keyDownHandler = function(e) {
+ this.isShiftDown = e.shiftKey;
+ this.isCtrlDown = e.ctrlKey;
+ this.isAltDown = e.altKey;
+ if (e.keyCode == 32) this.isSpaceDown = true;
+
+ if (!this.isShiftDown && this.isCtrlDown) {
+ if (!this.isAltDown) {
+ if (e.keyCode == 90 || e.keyCode == 85) this.undo(); //Ctrl+Z,Ctrl.U
+ if (e.keyCode == 89) this.redo(); //Ctrl+Y
+ } else {
+ if (e.keyCode == 90) this.redo(); //Ctrl+Alt+Z
+ }
+ }
+
+ if (!this.isShiftDown && !this.isCtrlDown && !this.isAltDown) {
+ if (e.keyCode == 107) new Neo.ZoomPlusCommand(this).execute(); // +
+ if (e.keyCode == 109) new Neo.ZoomMinusCommand(this).execute(); // -
+ }
+
+ if (this.tool.keyDownHandler) {
+ this.tool.keyDownHandler(e);
+ }
+
+ //スペース・Shift+スペースã§ã§ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã—ãªã„よã†ã«
+ if (document.activeElement != this.inputText) e.preventDefault();
+};
+
+Neo.Painter.prototype._keyUpHandler = function(e) {
+ this.isShiftDown = e.shiftKey;
+ this.isCtrlDown = e.ctrlKey;
+ this.isAltDown = e.altKey;
+ if (e.keyCode == 32) this.isSpaceDown = false;
+
+ if (this.tool.keyUpHandler) {
+ this.tool.keyUpHandler(oe);
+ }
+};
+
+Neo.Painter.prototype._rollOverHandler = function(e) {
+ if (this.tool.rollOverHandler) {
+ this.tool.rollOverHandler(this);
+ }
+};
+
+Neo.Painter.prototype._rollOutHandler = function(e) {
+ if (this.tool.rollOutHandler) {
+ this.tool.rollOutHandler(this);
+ }
+};
+
+Neo.Painter.prototype._mouseDownHandler = function(e) {
+ if (e.target == Neo.painter.destCanvas) {
+ //よãã‚ã‹ã‚‰ãªã„ãŒChromeã§ãƒ‰ãƒ©ãƒƒã‚°ã®æ™‚カレットãŒå‡ºã‚‹ã®ã‚’防ã
+ //http://stackoverflow.com/questions/2745028/chrome-sets-cursor-to-text-while-dragging-why
+ e.preventDefault();
+ }
+
+ if (e.type == "touchstart" && e.touches.length > 1) return;
+
+ if (e.button == 2 || this.virtualRight) {
+ this.isMouseDownRight = true;
+
+ } else {
+ if (!e.shiftKey && e.ctrlKey && e.altKey) {
+ this.isMouseDown = true;
+
+ } else {
+ if (e.ctrlKey || e.altKey) {
+ this.isMouseDownRight = true;
+ } else {
+ this.isMouseDown = true;
+ }
+ }
+ }
+
+ this._updateMousePosition(e);
+ this.prevMouseX = this.mouseX;
+ this.prevMouseY = this.mouseY;
+
+ if (this.isMouseDownRight) {
+ this.isMouseDownRight = false;
+ if (!this.isWidget(e.target)) {
+ this.pickColor(this.mouseX, this.mouseY);
+ return;
+ }
+ }
+
+ if (!this.isUIPaused()) {
+ if (e.target['data-bar']) {
+ this.pushTool(new Neo.HandTool());
+
+ } else if (this.isSpaceDown && document.activeElement != this.inputText) {
+ this.pushTool(new Neo.HandTool());
+ this.tool.reverse = true;
+
+ } else if (e.target['data-slider'] != undefined) {
+ this.pushTool(new Neo.SliderTool());
+ this.tool.target = e.target;
+
+ } else if (e.ctrlKey && e.altKey && !e.shiftKey) {
+ this.pushTool(new Neo.SliderTool());
+ this.tool.target = Neo.sliders[Neo.SLIDERTYPE_SIZE].element;
+ this.tool.alt = true;
+
+ } else if (this.isWidget(e.target)) {
+ this.isMouseDown = false;
+ this.pushTool(new Neo.DummyTool());
+
+ }
+ }
+
+// console.warn("down -" + e.target.id + e.target.className)
+ if (!(e.target.className == "o" && e.type == "touchdown")) {
+ this.tool.downHandler(this);
+ }
+
+// var ref = this;
+// document.onmouseup = function(e) {
+// ref._mouseUpHandler(e)
+// };
+};
+
+Neo.Painter.prototype._mouseUpHandler = function(e) {
+ this.isMouseDown = false;
+ this.isMouseDownRight = false;
+ this.tool.upHandler(this);
+// document.onmouseup = undefined;
+
+ if (e.target.id != "right") {
+ this.virtualRight = false;
+ Neo.RightButton.clear();
+ }
+
+// if (e.changedTouches) {
+// for (var i = 0; i < e.changedTouches.length; i++) {
+// var touch = e.changedTouches[i];
+// if (touch.identifier == this.touchModifier) {
+// this.touchModifier = null;
+// }
+// }
+// }
+};
+
+Neo.Painter.prototype._mouseMoveHandler = function(e) {
+ this._updateMousePosition(e);
+
+ if (e.type == "touchmove" && e.touches.length > 1) return;
+
+ if (this.isMouseDown || this.isMouseDownRight) {
+ this.tool.moveHandler(this);
+
+ } else {
+ if (this.tool.upMoveHandler) {
+ this.tool.upMoveHandler(this);
+ }
+ }
+
+ this.prevMouseX = this.mouseX;
+ this.prevMouseY = this.mouseY;
+
+ // ç”»é¢å¤–をタップã—ãŸæ™‚スクロールå¯èƒ½ã«ã™ã‚‹ãŸã‚
+// console.warn("move -" + e.target.id + e.target.className)
+ if (!(e.target.className == "o" && e.type == "touchmove")) {
+ e.preventDefault();
+ }
+};
+
+
+Neo.Painter.prototype.getPosition = function(e) {
+ if (e.clientX !== undefined) {
+ return {x: e.clientX, y: e.clientY, e: e.type};
+
+ } else {
+ var touch = e.changedTouches[0];
+ return {x: touch.clientX, y: touch.clientY, e: e.type};
+
+// for (var i = 0; i < e.changedTouches.length; i++) {
+// var touch = e.changedTouches[i];
+// if (!this.touchModifier || this.touchModifier != touch.identifier) {
+// return {x: touch.clientX, y: touch.clientY, e: e.type};
+// }
+// }
+// console.log("getPosition error");
+// return {x:0, y:0};
+ }
+}
+
+Neo.Painter.prototype._updateMousePosition = function(e) {
+ var rect = this.destCanvas.getBoundingClientRect();
+// var x = (e.clientX !== undefined) ? e.clientX : e.touches[0].clientX;
+// var y = (e.clientY !== undefined) ? e.clientY : e.touches[0].clientY;
+ var pos = this.getPosition(e);
+ var x = pos.x;
+ var y = pos.y;
+
+ if (this.zoom <= 0) this.zoom = 1; //ãªãœã‹0ã«ãªã‚‹ã“ã¨ãŒã‚ã‚‹ã®ã§
+
+ this.mouseX = (x - rect.left) / this.zoom
+ + this.zoomX
+ - this.destCanvas.width * 0.5 / this.zoom;
+ this.mouseY = (y - rect.top) / this.zoom
+ + this.zoomY
+ - this.destCanvas.height * 0.5 / this.zoom;
+
+ if (isNaN(this.prevMouseX)) {
+ this.prevMouseX = this.mouseX;
+ }
+ if (isNaN(this.prevMouseY)) {
+ this.prevMosueY = this.mouseY;
+ }
+
+ this.slowX = this.slowX * 0.8 + this.mouseX * 0.2;
+ this.slowY = this.slowY * 0.8 + this.mouseY * 0.2;
+ var now = new Date().getTime();
+ if (this.stab) {
+ var pause = this.stab[3];
+ if (pause) {
+ // ãƒãƒ¼ã‚ºä¸­
+ if (now > pause) {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+
+ } else {
+ // ãƒãƒ¼ã‚ºã•ã‚Œã¦ã„ãªã„ã¨ã
+ var prev = this.stab[2];
+ if (now - prev > 150) { // 150ms以上止ã¾ã£ã¦ã„ãŸã‚‰ãƒãƒ¼ã‚ºã‚’オンã«ã™ã‚‹
+ this.stab[3] = now + 200 // 200msペンã®ä½ç½®ã‚’固定
+
+ } else {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+ }
+ } else {
+ this.stab = [this.slowX, this.slowY, now];
+ }
+
+ this.rawMouseX = x;
+ this.rawMouseY = y;
+ this.clipMouseX = Math.max(Math.min(this.canvasWidth, this.mouseX), 0);
+ this.clipMouseY = Math.max(Math.min(this.canvasHeight, this.mouseY), 0);
+};
+
+Neo.Painter.prototype._beforeUnloadHandler = function(e) {
+ // quick save
+};
+
+Neo.Painter.prototype.getStabilized = function() {
+ return this.stab;
+};
+
+/*
+-------------------------------------------------------------------------
+ Undo
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.undo = function() {
+ var undoItem = this._undoMgr.popUndo();
+ if (undoItem) {
+ this._pushRedo();
+ this.canvasCtx[0].putImageData(undoItem.data[0], undoItem.x,undoItem.y);
+ this.canvasCtx[1].putImageData(undoItem.data[1], undoItem.x,undoItem.y);
+ this.updateDestCanvas(undoItem.x, undoItem.y, undoItem.width, undoItem.height);
+ }
+};
+
+Neo.Painter.prototype.redo = function() {
+ var undoItem = this._undoMgr.popRedo();
+ if (undoItem) {
+ this._pushUndo(0,0,this.canvasWidth, this.canvasHeight, true);
+ this.canvasCtx[0].putImageData(undoItem.data[0], undoItem.x,undoItem.y);
+ this.canvasCtx[1].putImageData(undoItem.data[1], undoItem.x,undoItem.y);
+ this.updateDestCanvas(undoItem.x, undoItem.y, undoItem.width, undoItem.height);
+ }
+};
+
+Neo.Painter.prototype.hasUndo = function() {
+ return true;
+};
+
+Neo.Painter.prototype._pushUndo = function(x, y, w, h, holdRedo) {
+ x = (x === undefined) ? 0 : x;
+ y = (y === undefined) ? 0 : y;
+ w = (w === undefined) ? this.canvasWidth : w;
+ h = (h === undefined) ? this.canvasHeight : h;
+ var undoItem = new Neo.UndoItem();
+ undoItem.x = 0;
+ undoItem.y = 0;
+ undoItem.width = w;
+ undoItem.height = h;
+ undoItem.data = [this.canvasCtx[0].getImageData(x, y, w, h),
+ this.canvasCtx[1].getImageData(x, y, w, h)];
+ this._undoMgr.pushUndo(undoItem, holdRedo);
+};
+
+Neo.Painter.prototype._pushRedo = function(x, y, w, h) {
+ x = (x === undefined) ? 0 : x;
+ y = (y === undefined) ? 0 : y;
+ w = (w === undefined) ? this.canvasWidth : w;
+ h = (h === undefined) ? this.canvasHeight : h;
+ var undoItem = new Neo.UndoItem();
+ undoItem.x = 0;
+ undoItem.y = 0;
+ undoItem.width = w;
+ undoItem.height = h;
+ undoItem.data = [this.canvasCtx[0].getImageData(x, y, w, h),
+ this.canvasCtx[1].getImageData(x, y, w, h)];
+ this._undoMgr.pushRedo(undoItem);
+};
+
+
+/*
+-------------------------------------------------------------------------
+ Data Cache for Undo / Redo
+-------------------------------------------------------------------------
+*/
+
+Neo.UndoManager = function(_maxStep){
+ this._maxStep = _maxStep;
+ this._undoItems = [];
+ this._redoItems = [];
+}
+Neo.UndoManager.prototype._maxStep;
+Neo.UndoManager.prototype._redoItems;
+Neo.UndoManager.prototype._undoItems;
+
+//アクションをã—ã¦Undo情報を更新
+Neo.UndoManager.prototype.pushUndo = function(undoItem, holdRedo) {
+ this._undoItems.push(undoItem);
+ if (this._undoItems.length > this._maxStep) {
+ this._undoItems.shift();
+ }
+
+ if (!holdRedo == true) {
+ this._redoItems = [];
+ }
+};
+
+Neo.UndoManager.prototype.popUndo = function() {
+ return this._undoItems.pop();
+}
+
+Neo.UndoManager.prototype.pushRedo = function(undoItem) {
+ this._redoItems.push(undoItem);
+}
+
+Neo.UndoManager.prototype.popRedo = function() {
+ return this._redoItems.pop();
+}
+
+
+Neo.UndoItem = function() {}
+Neo.UndoItem.prototype.data;
+Neo.UndoItem.prototype.x;
+Neo.UndoItem.prototype.y;
+Neo.UndoItem.prototype.width;
+Neo.UndoItem.prototype.height;
+
+/*
+-------------------------------------------------------------------------
+ Zoom Controller
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.setZoom = function(value) {
+ this.zoom = value;
+
+ var container = document.getElementById("container");
+ var width = this.canvasWidth * this.zoom;
+ var height = this.canvasHeight * this.zoom;
+ if (width > container.clientWidth - 100) width = container.clientWidth - 100;
+ if (height > container.clientHeight - 130) height = container.clientHeight - 130;
+ this.destWidth = width;
+ this.destHeight = height;
+
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight, false);
+ this.setZoomPosition(this.zoomX, this.zoomY);
+};
+
+Neo.Painter.prototype.setZoomPosition = function(x, y) {
+ var minx = (this.destCanvas.width / this.zoom) * 0.5;
+ var maxx = this.canvasWidth - minx;
+ var miny = (this.destCanvas.height / this.zoom) * 0.5;
+ var maxy = this.canvasHeight - miny;
+
+
+ x = Math.round(Math.max(Math.min(maxx,x),minx));
+ y = Math.round(Math.max(Math.min(maxy,y),miny));
+
+ this.zoomX = x;
+ this.zoomY = y;
+ this.updateDestCanvas(0,0,this.canvasWidth,this.canvasHeight,false);
+
+ this.scrollBarX = (maxx == minx) ? 0 : (x - minx) / (maxx - minx);
+ this.scrollBarY = (maxy == miny) ? 0 : (y - miny) / (maxy - miny);
+ this.scrollWidth = maxx - minx;
+ this.scrollHeight = maxy - miny;
+
+ if (Neo.scrollH) Neo.scrollH.update(this);
+ if (Neo.scrollV) Neo.scrollV.update(this);
+
+ this.hideInputText();
+};
+
+
+/*
+-------------------------------------------------------------------------
+ Drawing Helper
+-------------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.submit = function(board) {
+ var thumbnail = null;
+ var thumbnail2 = null;
+
+ if (this.useThumbnail()) {
+ thumbnail = this.getThumbnail(Neo.config.thumbnail_type || "png");
+ if (Neo.config.thumbnail_type2) {
+ thumbnail2 = this.getThumbnail(Neo.config.thumbnail_type2);
+ }
+ }
+ Neo.submit(board, this.getPNG(), thumbnail2, thumbnail);
+};
+
+Neo.Painter.prototype.useThumbnail = function() {
+ var thumbnailWidth = this.getThumbnailWidth();
+ var thumbnailHeight = this.getThumbnailHeight();
+ if (thumbnailWidth && thumbnailHeight) {
+ if (thumbnailWidth < this.canvasWidth ||
+ thumbnailHeight < this.canvasHeight) {
+ return true;
+ }
+ }
+ return false;
+};
+
+Neo.Painter.prototype.dataURLtoBlob = function(dataURL) {
+ var byteString;
+ if (dataURL.split(',')[0].indexOf('base64') >= 0) {
+ byteString = atob(dataURL.split(',')[1]);
+ } else {
+ byteString = unescape(dataURL.split(',')[1]);
+ }
+
+ // write the bytes of the string to a typed array
+ var ia = new Uint8Array(byteString.length);
+ for (var i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
+ }
+ return new Blob([ia], {type:'image/png'});
+};
+
+Neo.Painter.prototype.getImage = function(imageWidth, imageHeight) {
+ var width = this.canvasWidth;
+ var height = this.canvasHeight;
+ imageWidth = imageWidth || width;
+ imageHeight = imageHeight || height;
+
+ var pngCanvas = document.createElement("canvas");
+ pngCanvas.width = imageWidth;
+ pngCanvas.height = imageHeight;
+ var pngCanvasCtx = pngCanvas.getContext("2d");
+ pngCanvasCtx.fillStyle = "#ffffff";
+ pngCanvasCtx.fillRect(0, 0, imageWidth, imageHeight);
+
+ if (this.visible[0]) {
+ pngCanvasCtx.drawImage(this.canvas[0],
+ 0, 0, width, height,
+ 0, 0, imageWidth, imageHeight);
+ }
+ if (this.visible[1]) {
+ pngCanvasCtx.drawImage(this.canvas[1],
+ 0, 0, width, height,
+ 0, 0, imageWidth, imageHeight);
+ }
+ return pngCanvas;
+};
+
+Neo.Painter.prototype.getPNG = function() {
+ var image = this.getImage();
+ var dataURL = image.toDataURL('image/png');
+ return this.dataURLtoBlob(dataURL);
+};
+
+Neo.Painter.prototype.getThumbnail = function(type) {
+ if (type != "animation") {
+ var thumbnailWidth = this.getThumbnailWidth();
+ var thumbnailHeight = this.getThumbnailHeight();
+ if (thumbnailWidth || thumbnailHeight) {
+ var width = this.canvasWidth;
+ var height = this.canvasHeight;
+ if (thumbnailWidth == 0) {
+ thumbnailWidth = thumbnailHeight * width / height;
+ }
+ if (thumbnailHeight == 0) {
+ thumbnailHeight = thumbnailWidth * height / width;
+ }
+ } else {
+ thumbnailWidth = thumbnailHeight = null;
+ }
+
+ console.log("get thumbnail", thumbnailWidth, thumbnailHeight);
+
+ var image = this.getImage(thumbnailWidth, thumbnailHeight);
+ var dataURL = image.toDataURL('image/' + type);
+ return this.dataURLtoBlob(dataURL);
+
+ } else {
+ return new Blob([]); //animationã«ã¯å¯¾å¿œã—ã¦ã„ãªã„ã®ã§ãƒ€ãƒŸãƒ¼ãƒ‡ãƒ¼ã‚¿ã‚’è¿”ã™
+ }
+};
+
+Neo.Painter.prototype.getThumbnailWidth = function() {
+ var width = Neo.config.thumbnail_width;
+ if (width) {
+ if (width.match(/%$/)) {
+ return Math.floor(this.canvasWidth * (parseInt(width) / 100.0));
+ } else {
+ return parseInt(width);
+ }
+ }
+ return 0;
+};
+
+Neo.Painter.prototype.getThumbnailHeight = function() {
+ var height = Neo.config.thumbnail_height;
+ if (height) {
+ if (height.match(/%$/)) {
+ return Math.floor(this.canvasHeight * (parseInt(height) / 100.0));
+ } else {
+ return parseInt(height);
+ }
+ }
+ return 0;
+};
+
+Neo.Painter.prototype.clearCanvas = function(doConfirm) {
+ if (!doConfirm || confirm("全消ã—ã—ã¾ã™")) {
+ //Register undo first;
+ this._pushUndo();
+
+ this.canvasCtx[0].clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ this.canvasCtx[1].clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+ }
+};
+
+Neo.Painter.prototype.updateDestCanvas = function(x, y, width, height, useTemp) {
+ var canvasWidth = this.canvasWidth;
+ var canvasHeight = this.canvasHeight;
+ var updateAll = false;
+ if (x == 0 && y == 0 && width == canvasWidth && height == canvasHeight) {
+ updateAll = true;
+ };
+
+ if (x + width > this.canvasWidth) width = this.canvasWidth - x;
+ if (y + height > this.canvasHeight) height = this.canvasHeight - y;
+ if (x < 0) x = 0;
+ if (y < 0) y = 0;
+ if (width <= 0 || height <= 0) return;
+
+ var ctx = this.destCanvasCtx;
+ ctx.save();
+ ctx.fillStyle = "#ffffff";
+
+ var fillWidth = width
+ var fillHeight = height
+
+ if (updateAll) {
+ ctx.fillRect(0, 0, this.destCanvas.width, this.destCanvas.height);
+
+ } else {
+ //カーソルã®æ画ゴミãŒæ®‹ã‚‹ã®ã‚’ã”ã¾ã‹ã™ãŸã‚
+ if (x + width == this.canvasWidth) fillWidth = width + 1;
+ if (y + height == this.canvasHeight) fillHeight = height + 1;
+ }
+
+ ctx.translate(this.destCanvas.width*.5, this.destCanvas.height*.5);
+ ctx.scale(this.zoom, this.zoom);
+ ctx.translate(-this.zoomX, -this.zoomY);
+ ctx.globalAlpha = 1.0;
+ ctx.msImageSmoothingEnabled = 0;
+
+ if (!updateAll) {
+ ctx.fillRect(x, y, fillWidth, fillHeight);
+ }
+
+ if (this.visible[0]) {
+ ctx.drawImage(this.canvas[0],
+ x, y, width, height,
+ x, y, width, height);
+ }
+ if (this.visible[1]) {
+ ctx.drawImage(this.canvas[1],
+ x, y, width, height,
+ x, y, width, height);
+ }
+ if (useTemp) {
+ ctx.globalAlpha = 1.0; //this.alpha;
+ ctx.drawImage(this.tempCanvas,
+ x, y, width, height,
+ x + this.tempX, y + this.tempY, width, height);
+ }
+ ctx.restore();
+};
+
+Neo.Painter.prototype.getBound = function(x0, y0, x1, y1, r) {
+ var left = Math.floor((x0 < x1) ? x0 : x1);
+ var top = Math.floor((y0 < y1) ? y0 : y1);
+ var width = Math.ceil(Math.abs(x0 - x1));
+ var height = Math.ceil(Math.abs(y0 - y1));
+ r = Math.ceil(r + 1);
+
+ if (!r) {
+ width += 1;
+ height += 1;
+
+ } else {
+ left -= r;
+ top -= r;
+ width += r * 2;
+ height += r * 2;
+ }
+ return [left, top, width, height];
+};
+
+Neo.Painter.prototype.getColor = function(c) {
+ if (!c) c = this.foregroundColor;
+ var r = parseInt(c.substr(1, 2), 16);
+ var g = parseInt(c.substr(3, 2), 16);
+ var b = parseInt(c.substr(5, 2), 16);
+ var a = Math.floor(this.alpha * 255);
+ return a <<24 | b<<16 | g<<8 | r;
+};
+
+Neo.Painter.prototype.getColorString = function(c) {
+ var rgb = ("000000" + (c & 0xffffff).toString(16)).substr(-6);
+ return '#' + rgb;
+};
+
+Neo.Painter.prototype.setColor = function(c) {
+ if (typeof c != "string") c = this.getColorString(c);
+ this.foregroundColor = c;
+
+ Neo.updateUI();
+};
+
+Neo.Painter.prototype.getAlpha = function(type) {
+ var a1 = this.alpha;
+
+ switch (type) {
+ case Neo.Painter.ALPHATYPE_PEN:
+ if (a1 > 0.5) {
+ a1 = 1.0/16 + (a1 - 0.5) * 30.0/16;
+ } else {
+ a1 = Math.sqrt(2 * a1) / 16.0;
+ }
+ a1 = Math.min(1, Math.max(0, a1));
+ break;
+
+ case Neo.Painter.ALPHATYPE_FILL:
+ a1 = -0.00056 * a1 + 0.0042 / (1.0 - a1) - 0.0042;
+ a1 = Math.min(1.0, Math.max(0, a1 * 10));
+ break;
+
+ case Neo.Painter.ALPHATYPE_BRUSH:
+ a1 = -0.00056 * a1 + 0.0042 / (1.0 - a1) - 0.0042;
+ a1 = Math.min(1.0, Math.max(0, a1));
+ break;
+ }
+
+ // アルファãŒå°ã•ã„時ã¯é©å½“ã«ç‚¹ã‚’抜ã„ã¦è¦‹ãŸç›®ã®æ¿ƒåº¦ã‚’åˆã‚ã›ã‚‹
+ if (a1 < 1.0/255) {
+ this.aerr += a1;
+ a1 = 0;
+ while (this.aerr > 1.0/255) {
+ a1 = 1.0/255;
+ this.aerr -= 1.0/255;
+ }
+ }
+ return a1;
+};
+
+Neo.Painter.prototype.prepareDrawing = function () {
+ var r = parseInt(this.foregroundColor.substr(1, 2), 16);
+ var g = parseInt(this.foregroundColor.substr(3, 2), 16);
+ var b = parseInt(this.foregroundColor.substr(5, 2), 16);
+ var a = Math.floor(this.alpha * 255);
+
+ var maskR = parseInt(this.maskColor.substr(1, 2), 16);
+ var maskG = parseInt(this.maskColor.substr(3, 2), 16);
+ var maskB = parseInt(this.maskColor.substr(5, 2), 16);
+
+ this._currentColor = [r, g, b, a];
+ this._currentMask = [maskR, maskG, maskB];
+};
+
+Neo.Painter.prototype.isMasked = function (buf8, index) {
+ var r = this._currentMask[0];
+ var g = this._currentMask[1];
+ var b = this._currentMask[2];
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3];
+
+ if (a0 == 0) {
+ r0 = 0xff;
+ g0 = 0xff;
+ b0 = 0xff;
+ }
+
+ var type = this.maskType;
+
+ //TODO
+ //ã„ã‚ã„ã‚試ã—ãŸã®ã§ã™ãŒåŠé€æ˜Žã§æç”»ã™ã‚‹ã¨ãã®åŠ ç®—・逆加算をå†ç¾ã™ã‚‹æ–¹æ³•ãŒã‚ã‹ã‚Šã¾ã›ã‚“。
+ //ã¨ã‚Šã‚ãˆãšå˜ç´”ã«ç„¡è¦–ã—ã¦ã„ã¾ã™ã€‚
+ if (type == Neo.Painter.MASKTYPE_ADD ||
+ type == Neo.Painter.MASKTYPE_SUB) {
+ if (this._currentColor[3] < 250) {
+ type = Neo.Painter.MASKTYPE_NONE;
+ }
+ }
+
+ switch (type) {
+ case Neo.Painter.MASKTYPE_NONE:
+ return;
+
+ case Neo.Painter.MASKTYPE_NORMAL:
+ return (r0 == r &&
+ g0 == g &&
+ b0 == b) ? true : false;
+
+ case Neo.Painter.MASKTYPE_REVERSE:
+ return (r0 != r ||
+ g0 != g ||
+ b0 != b) ? true : false;
+
+ case Neo.Painter.MASKTYPE_ADD:
+ if (a0 > 0) {
+ var sort = this.sortColor(r0, g0, b0);
+ for (var i = 0; i < 3; i++) {
+ var c = sort[i];
+ if (buf8[index + c] < this._currentColor[c]) return true;
+ }
+ return false;
+
+ } else {
+ return false;
+ }
+
+ case Neo.Painter.MASKTYPE_SUB:
+ if (a0 > 0) {
+ var sort = this.sortColor(r0, g0, b0);
+ for (var i = 0; i < 3; i++) {
+ var c = sort[i];
+ if (buf8[index + c] > this._currentColor[c]) return true;
+ }
+ return false;
+ } else {
+ return true;
+ }
+ }
+};
+
+Neo.Painter.prototype.setPoint = function(buf8, bufWidth, x0, y0, left, top, type) {
+ var x = x0 - left;
+ var y = y0 - top;
+
+ switch (type) {
+ case Neo.Painter.LINETYPE_PEN:
+ this.setPenPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BRUSH:
+ this.setBrushPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_TONE:
+ this.setTonePoint(buf8, bufWidth, x, y, x0, y0);
+ break;
+
+ case Neo.Painter.LINETYPE_ERASER:
+ this.setEraserPoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BLUR:
+ this.setBlurPoint(buf8, bufWidth, x, y, x0, y0);
+ break;
+
+ case Neo.Painter.LINETYPE_DODGE:
+ this.setDodgePoint(buf8, bufWidth, x, y);
+ break;
+
+ case Neo.Painter.LINETYPE_BURN:
+ this.setBurnPoint(buf8, bufWidth, x, y);
+ break;
+
+ default:
+ break;
+ }
+};
+
+
+Neo.Painter.prototype.setPenPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_PEN);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var a1x = Math.max(a1, 1.0/255);
+
+ var r = (r1 * a1x + r0 * a0 * (1 - a1x)) / a;
+ var g = (g1 * a1x + g0 * a0 * (1 - a1x)) / a;
+ var b = (b1 * a1x + b0 * a0 * (1 - a1x)) / a;
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBrushPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var a1x = Math.max(a1, 1.0/255);
+
+ var r = (r1 * a1x + r0 * a0) / (a0 + a1x);
+ var g = (g1 * a1x + g0 * a0) / (a0 + a1x);
+ var b = (b1 * a1x + b0 * a0) / (a0 + a1x);
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setTonePoint = function(buf8, width, x, y, x0, y0) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+
+ x -= r0;
+ y -= r0;
+ x0 -= r0;
+ y0 -= r0;
+// x -= r0;
+// y -= r0;
+// if (r0%2) { x0++; y0++; } //ãªãœã‹æ¨¡æ§˜ãŒãšã‚Œã‚‹ã®ã§
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var index = (y * width + x) * 4;
+
+ var r = this._currentColor[0];
+ var g = this._currentColor[1];
+ var b = this._currentColor[2];
+ var a = this._currentColor[3];
+
+ var toneData = this.getToneData(a);
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ if (toneData[((y0+i)%4) + (((x0+j)%4) * 4)]) {
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = 255;
+ }
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setEraserPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var index = (y * width + x) * 4;
+ var a = Math.floor(this.alpha * 255);
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var k = (buf8[index + 3] / 255.0) * (1.0 - (a / 255.0));
+
+ buf8[index + 3] -= a / (d * (255.0 - a) / 255.0);
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBlurPoint = function(buf8, width, x, y, x0, y0) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+ var height = buf8.length / (width * 4);
+
+// var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ var a1 = this.alpha / 12;
+ if (a1 == 0) return;
+ var blur = a1;
+
+ var tmp = new Uint8ClampedArray(buf8.length);
+ for (var i = 0; i < buf8.length; i++) {
+ tmp[i] = buf8[i];
+ }
+
+ var left = x0 - x - r0;
+ var top = y0 - y - r0;
+
+ var xstart = 0, xend = d;
+ var ystart = 0, yend = d;
+ if (xstart > left) xstart = -left;
+ if (ystart > top) ystart = -top;
+ if (xend > this.canvasWidth - left) xend = this.canvasWidth - left;
+ if (yend > this.canvasHeight - top) yend = this.canvasHeight - top;
+
+ for (var j = ystart; j < yend; j++) {
+ var index = (j * width + xstart) * 4;
+ for (var i = xstart; i < xend; i++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var rgba = [0, 0, 0, 0, 0];
+
+ this.addBlur(tmp, index, 1.0 - blur*4, rgba);
+ if (i > xstart) this.addBlur(tmp, index - 4, blur, rgba);
+ if (i < xend - 1) this.addBlur(tmp, index + 4, blur, rgba);
+ if (j > ystart) this.addBlur(tmp, index - width*4, blur, rgba);
+ if (j < yend - 1) this.addBlur(tmp, index + width*4, blur, rgba);
+
+ buf8[index + 0] = Math.round(rgba[0]);
+ buf8[index + 1] = Math.round(rgba[1]);
+ buf8[index + 2] = Math.round(rgba[2]);
+ buf8[index + 3] = Math.round((rgba[3] / rgba[4]) * 255.0);
+ }
+ index += 4;
+ }
+ }
+};
+
+Neo.Painter.prototype.setDodgePoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ if (a1 != 255.0) {
+ var r1 = r0 * 255 / (255 - a1);
+ var g1 = g0 * 255 / (255 - a1);
+ var b1 = b0 * 255 / (255 - a1);
+ } else {
+ var r1 = 255.0;
+ var g1 = 255.0;
+ var b1 = 255.0;
+ }
+
+ var r = Math.ceil(r1);
+ var g = Math.ceil(g1);
+ var b = Math.ceil(b1);
+ var a = a0;
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+Neo.Painter.prototype.setBurnPoint = function(buf8, width, x, y) {
+ var d = this.lineWidth;
+ var r0 = Math.floor(d / 2);
+ x -= r0;
+ y -= r0;
+
+ var index = (y * width + x) * 4;
+
+ var shape = this._roundData[d];
+ var shapeIndex = 0;
+
+ var a1 = this.getAlpha(Neo.Painter.ALPHATYPE_BRUSH);
+ if (a1 == 0) return;
+
+ for (var i = 0; i < d; i++) {
+ for (var j = 0; j < d; j++) {
+ if (shape[shapeIndex++] && !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ if (a1 != 255.0) {
+ var r1 = 255 - (255 - r0) * 255 / (255 - a1);
+ var g1 = 255 - (255 - g0) * 255 / (255 - a1);
+ var b1 = 255 - (255 - b0) * 255 / (255 - a1);
+ } else {
+ var r1 = 0;
+ var g1 = 0;
+ var b1 = 0;
+ }
+
+ var r = Math.floor(r1);
+ var g = Math.floor(g1);
+ var b = Math.floor(b1);
+ var a = a0;
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ }
+ index += 4;
+ }
+ index += (width - d) * 4;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////
+
+Neo.Painter.prototype.xorPixel = function(buf32, bufWidth, x, y, c) {
+ var index = y * bufWidth + x;
+ if (!c) c = 0xffffff;
+ buf32[index] ^= c;
+};
+
+Neo.Painter.prototype.getBezierPoint = function(t, x0, y0, x1, y1, x2, y2, x3, y3) {
+ var a0 = (1 - t) * (1 - t) * (1 - t);
+ var a1 = (1 - t) * (1 - t) * t * 3;
+ var a2 = (1 - t) * t * t * 3;
+ var a3 = t * t * t;
+
+ var x = x0 * a0 + x1 * a1 + x2 * a2 + x3 * a3;
+ var y = y0 * a0 + y1 * a1 + y2 * a2 + y3 * a3;
+ return [x, y];
+};
+
+var nmax = 1;
+
+Neo.Painter.prototype.drawBezier = function(ctx, x0, y0, x1, y1, x2, y2, x3, y3, type) {
+ var xmax = Math.max(x0, x1, x2, x3);
+ var xmin = Math.min(x0, x1, x2, x3);
+ var ymax = Math.max(y0, y1, y2, y3);
+ var ymin = Math.min(y0, y1, y2, y3);
+ var n = Math.ceil(((xmax - xmin) + (ymax - ymin)) * 2.5);
+
+ if (n > nmax) {
+ n = (n < nmax * 2) ? n : nmax * 2;
+ nmax = n;
+ }
+
+ for (var i = 0; i < n; i++) {
+ var t = i * 1.0 / n;
+ var p = this.getBezierPoint(t, x0, y0, x1, y1, x2, y2, x3, y3);
+ this.drawPoint(ctx, p[0], p[1], type);
+ }
+};
+
+Neo.Painter.prototype.prevLine = null; // 始点ã¾ãŸã¯çµ‚点ãŒ2度プロットã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹ã®ã§
+Neo.Painter.prototype.drawLine = function(ctx, x0, y0, x1, y1, type) {
+ x0 = Math.round(x0);
+ x1 = Math.round(x1);
+ y0 = Math.round(y0);
+ y1 = Math.round(y1);
+ var prev = [x0, y0, x1, y1];
+
+ var width = Math.abs(x1 - x0);
+ var height = Math.abs(y1 - y0);
+ var r = Math.ceil(this.lineWidth / 2);
+
+ var left = ((x0 < x1) ? x0 : x1) - r;
+ var top = ((y0 < y1) ? y0 : y1) - r;
+
+ var imageData = ctx.getImageData(left, top, width + r*2, height + r*2);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var dx = width, sx = x0 < x1 ? 1 : -1;
+ var dy = height, sy = y0 < y1 ? 1 : -1;
+ var err = (dx > dy ? dx : -dy) / 2;
+ this.aerr = 0;
+
+ while (true) {
+ if (this.prevLine == null ||
+ !((this.prevLine[0] == x0 && this.prevLine[1] == y0) ||
+ (this.prevLine[2] == x0 && this.prevLine[3] == y0))) {
+ this.setPoint(buf8, imageData.width, x0, y0, left, top, type);
+ }
+
+ if (x0 === x1 && y0 === y1) break;
+ var e2 = err;
+ if (e2 > -dx) { err -= dy; x0 += sx; }
+ if (e2 < dy) { err += dx; y0 += sy; }
+ }
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, left, top);
+
+ this.prevLine = prev;
+};
+
+Neo.Painter.prototype.drawPoint = function(ctx, x, y, type) {
+ this.drawLine(ctx, x, y, x, y, type);
+};
+
+Neo.Painter.prototype.xorRect = function(buf32, bufWidth, x, y, width, height, c) {
+ var index = y * bufWidth + x;
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ buf32[index] ^= c;
+ index++;
+ }
+ index += width - bufWidth;
+ }
+};
+
+Neo.Painter.prototype.drawXORRect = function(ctx, x, y, width, height, isFill, c) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+ if (width == 0 || height == 0) return;
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var index = 0;
+ if (!c) c = 0xffffff;
+
+ if (isFill) {
+ this.xorRect(buf32, width, 0, 0, width, height, c);
+
+ } else {
+ for (var i = 0; i < width; i++) { //top
+ buf32[index] = buf32[index] ^= c;
+ index++;
+ }
+ if (height > 1) {
+ index = width;
+ for (var i = 1; i < height; i++) { //left
+ buf32[index] = buf32[index] ^= c;
+ index += width;
+ }
+ if (width > 1) {
+ index = width * 2 - 1;
+ for (var i = 1; i < height - 1; i++) { //right
+ buf32[index] = buf32[index] ^= c;
+ index += width;
+ }
+ index = width * (height - 1) + 1;
+ for (var i = 1; i < width; i++) { // bottom
+ buf32[index] = buf32[index] ^= c;
+ index++;
+ }
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.drawXOREllipse = function(ctx, x, y, width, height, isFill, c) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+ if (width == 0 || height == 0) return;
+ if (!c) c = 0xffffff;
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+
+ var a = width-1, b = height-1, b1 = b&1; /* values of diameter */
+ var dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */
+ var err = dx+dy+b1*a*a, e2; /* error of 1.step */
+
+ var x0 = x;
+ var y0 = y;
+ var x1 = x0+a;
+ var y1 = y0+b;
+
+ if (x0 > x1) { x0 = x1; x1 += a; }
+ if (y0 > y1) y0 = y1;
+ y0 += Math.floor((b+1)/2); y1 = y0-b1; /* starting pixel */
+ a *= 8*a; b1 = 8*b*b;
+ var ymin = y0 - 1;
+
+ do {
+ if (isFill) {
+ if (ymin < y0) {
+ this.xorRect(buf32, width, x0-x, y0 - y, x1 - x0, 1, c);
+ if (y0 != y1) {
+ this.xorRect(buf32, width, x0-x, y1 - y, x1 - x0, 1, c);
+ }
+ ymin = y0;
+ }
+ } else {
+ this.xorPixel(buf32, width, x1-x, y0-y, c);
+ if (x0 != x1) {
+ this.xorPixel(buf32, width, x0-x, y0-y, c);
+ }
+ if (y0 != y1) {
+ this.xorPixel(buf32, width, x0-x, y1-y, c);
+ if (x0 != x1) {
+ this.xorPixel(buf32, width, x1-x, y1-y, c);
+ }
+ }
+ }
+ e2 = 2*err;
+ if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */
+ if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */
+ } while (x0 <= x1);
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.drawXORLine = function(ctx, x0, y0, x1, y1, c) {
+ x0 = Math.round(x0);
+ x1 = Math.round(x1);
+ y0 = Math.round(y0);
+ y1 = Math.round(y1);
+
+ var width = Math.abs(x1 - x0);
+ var height = Math.abs(y1 - y0);
+
+ var left = ((x0 < x1) ? x0 : x1);
+ var top = ((y0 < y1) ? y0 : y1);
+// console.log("left:"+left+" top:"+top+" width:"+width+" height:"+height);
+
+ var imageData = ctx.getImageData(left, top, width + 1, height + 1);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var dx = width, sx = x0 < x1 ? 1 : -1;
+ var dy = height, sy = y0 < y1 ? 1 : -1;
+ var err = (dx > dy ? dx : -dy) / 2;
+
+ while (true) {
+ if (this.prevLine == null ||
+ !((this.prevLine[0] == x0 && this.prevLine[1] == y0) ||
+ (this.prevLine[2] == x0 && this.prevLine[3] == y0))) {
+
+ this.xorPixel(buf32, imageData.width, x0 - left, y0 - top, c);
+ }
+
+ if (x0 === x1 && y0 === y1) break;
+ var e2 = err;
+ if (e2 > -dx) { err -= dy; x0 += sx; }
+ if (e2 < dy) { err += dx; y0 += sy; }
+ }
+
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, left, top);
+};
+
+
+Neo.Painter.prototype.eraseRect = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var index = 0;
+
+ var a = 1.0 - this.alpha;
+ if (a != 0) {
+ a = Math.ceil(2.0 / a);
+ } else {
+ a = 255;
+ }
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ if (!this.isMasked(buf8, index)) {
+ buf8[index + 3] -= a;
+ }
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.flipH = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var half = Math.floor(width / 2);
+ for (var j = 0; j < height; j++) {
+ var index = j * width;
+ var index2 = index + (width - 1);
+ for (var i = 0; i < half; i++) {
+ var value = buf32[index + i];
+ buf32[index + i] = buf32[index2 -i];
+ buf32[index2 - i] = value;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.flipV = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var half = Math.floor(height / 2);
+ for (var j = 0; j < half; j++) {
+ var index = j * width;
+ var index2 = (height - 1 - j) * width;
+ for (var i = 0; i < width; i++) {
+ var value = buf32[index + i];
+ buf32[index + i] = buf32[index2 + i];
+ buf32[index2 + i] = value;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.merge = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = [];
+ var buf32 = [];
+ var buf8 = [];
+ for (var i = 0; i < 2; i++) {
+ imageData[i] = this.canvasCtx[i].getImageData(x, y, width, height);
+ buf32[i] = new Uint32Array(imageData[i].data.buffer);
+ buf8[i] = new Uint8ClampedArray(imageData[i].data.buffer);
+ }
+
+ var dst = this.current;
+ var src = (dst == 1) ? 0 : 1;
+ var size = width * height;
+ var index = 0;
+ for (var i = 0; i < size; i++) {
+ var r0 = buf8[0][index + 0];
+ var g0 = buf8[0][index + 1];
+ var b0 = buf8[0][index + 2];
+ var a0 = buf8[0][index + 3] / 255.0;
+ var r1 = buf8[1][index + 0];
+ var g1 = buf8[1][index + 1];
+ var b1 = buf8[1][index + 2];
+ var a1 = buf8[1][index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+ if (a > 0) {
+ var r = Math.floor((r1 * a1 + r0 * a0 * (1 - a1)) / a + 0.5);
+ var g = Math.floor((g1 * a1 + g0 * a0 * (1 - a1)) / a + 0.5);
+ var b = Math.floor((b1 * a1 + b0 * a0 * (1 - a1)) / a + 0.5);
+ }
+ buf8[src][index + 0] = 0;
+ buf8[src][index + 1] = 0;
+ buf8[src][index + 2] = 0;
+ buf8[src][index + 3] = 0;
+ buf8[dst][index + 0] = r;
+ buf8[dst][index + 1] = g;
+ buf8[dst][index + 2] = b;
+ buf8[dst][index + 3] = Math.floor(a * 255 + 0.5);
+ index += 4;
+ }
+
+ for (var i = 0; i < 2; i++) {
+ imageData[i].data.set(buf8[i]);
+ this.canvasCtx[i].putImageData(imageData[i], x, y);
+ }
+};
+
+Neo.Painter.prototype.blurRect = function(ctx, x, y, width, height) {
+ x = Math.round(x);
+ y = Math.round(y);
+ width = Math.round(width);
+ height = Math.round(height);
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var tmp = new Uint8ClampedArray(buf8.length);
+ for (var i = 0; i < buf8.length; i++) tmp[i] = buf8[i];
+
+ var index = 0;
+ var a1 = this.alpha / 12;
+ var blur = a1;
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ var rgba = [0, 0, 0, 0, 0];
+
+ this.addBlur(tmp, index, 1.0 - blur*4, rgba);
+
+ if (i > 0) this.addBlur(tmp, index - 4, blur, rgba);
+ if (i < width - 1) this.addBlur(tmp, index + 4, blur, rgba);
+ if (j > 0) this.addBlur(tmp, index - width*4, blur, rgba);
+ if (j < height - 1) this.addBlur(tmp, index + width*4, blur, rgba);
+
+ var w = rgba[4];
+ buf8[index + 0] = Math.round(rgba[0]);
+ buf8[index + 1] = Math.round(rgba[1]);
+ buf8[index + 2] = Math.round(rgba[2]);
+ buf8[index + 3] = Math.ceil((rgba[3] / w) * 255.0);
+
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.addBlur = function(buffer, index, a, rgba) {
+ var r0 = rgba[0];
+ var g0 = rgba[1];
+ var b0 = rgba[2];
+ var a0 = rgba[3];
+ var r1 = buffer[index + 0];
+ var g1 = buffer[index + 1];
+ var b1 = buffer[index + 2];
+ var a1 = (buffer[index + 3] / 255.0) * a;
+ rgba[4] += a;
+
+ var a = a0 + a1;
+ if (a > 0) {
+ rgba[0] = (r1 * a1 + r0 * a0) / (a0 + a1);
+ rgba[1] = (g1 * a1 + g0 * a0) / (a0 + a1);
+ rgba[2] = (b1 * a1 + b0 * a0) / (a0 + a1);
+ rgba[3] = a;
+ }
+};
+
+Neo.Painter.prototype.pickColor = function(x, y) {
+ var r = 0xff, g = 0xff, b = 0xff, a;
+
+ x = Math.floor(x);
+ y = Math.floor(y);
+ if (x >= 0 && x < this.canvasWidth &&
+ y >= 0 && y < this.canvasHeight) {
+
+ for (var i = 0; i < 2; i++) {
+ if (this.visible[i]) {
+ var ctx = this.canvasCtx[i];
+ var imageData = ctx.getImageData(x, y, 1, 1);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var a = buf8[3] / 255.0;
+ r = r * (1.0 - a) + buf8[2] * a;
+ g = g * (1.0 - a) + buf8[1] * a;
+ b = b * (1.0 - a) + buf8[0] * a;
+ }
+ }
+ r = Math.max(Math.min(Math.round(r), 255), 0);
+ g = Math.max(Math.min(Math.round(g), 255), 0);
+ b = Math.max(Math.min(Math.round(b), 255), 0);
+ var result = r | g<<8 | b<<16;
+ }
+ this.setColor(result);
+
+
+ if (this.current > 0) {
+ if (a == 0 && (result == 0xffffff || this.getEmulationMode() < 2.16)) {
+ this.setToolByType(Neo.eraserTip.tools[Neo.eraserTip.mode]);
+
+ } else {
+ if (Neo.eraserTip.selected) {
+ this.setToolByType(Neo.penTip.tools[Neo.penTip.mode]);
+ }
+ }
+ }
+};
+
+Neo.Painter.prototype.fillHorizontalLine = function(buf32, x0, x1, y) {
+ var index = y * this.canvasWidth + x0;
+ var fillColor = this.getColor();
+ for (var x = x0; x <= x1; x++) {
+ buf32[index++] = fillColor;
+ }
+};
+
+Neo.Painter.prototype.scanLine = function(x0, x1, y, baseColor, buf32, stack) {
+ var width = this.canvasWidth;
+ for (var x = x0; x <= x1; x++) {
+ stack.push({x:x, y: y})
+ }
+/*
+ while (x0 <= x1) {
+ for (; x0 <= x1; x0++) {
+ if (buf32[y * width + x0] == baseColor) break;
+ }
+ if (x1 < x0) break;
+
+ for (; x0 <= x1; x0++) {
+ if (buf32[y * width + x0] != baseColor) break;
+ }
+ stack.push({x:x0 - 1, y: y})
+ }
+*/
+};
+
+Neo.Painter.prototype.fill = function(x, y, ctx) {
+ x = Math.round(x);
+ y = Math.round(y);
+
+ if (x < 0 || x >= this.canvasWidth || y < 0 || y >= this.canvasHeight) {
+ return;
+ }
+
+ var imageData = ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var width = imageData.width;
+ var stack = [{x: x, y: y}];
+
+ var baseColor = buf32[y * width + x];
+ var fillColor = this.getColor();
+
+ if ((baseColor & 0xff000000) == 0 || (baseColor != fillColor)) {
+ while (stack.length > 0) {
+ if (stack.length > 1000000) {
+ console.log('too much stack')
+ break;
+ }
+ var point = stack.pop();
+ var x = point.x;
+ var y = point.y;
+ var x0 = x;
+ var x1 = x;
+ if (buf32[y * width + x] == fillColor) continue;
+ if (buf32[y * width + x] != baseColor) continue;
+
+ for (; 0 < x0; x0--) {
+ if (buf32[y * width + (x0 - 1)] != baseColor) break;
+ }
+ for (; x1 < this.canvasWidth - 1; x1++) {
+ if (buf32[y * width + (x1 + 1)] != baseColor) break;
+ }
+ this.fillHorizontalLine(buf32, x0, x1, y);
+
+ if (y + 1 < this.canvasHeight) {
+ this.scanLine(x0, x1, y + 1, baseColor, buf32, stack);
+ }
+ if (y - 1 >= 0) {
+ this.scanLine(x0, x1, y - 1, baseColor, buf32, stack);
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+ this.updateDestCanvas(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.copy = function(x, y, width, height) {
+ this.tempX = 0;
+ this.tempY = 0;
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+
+ var imageData = this.canvasCtx[this.current].getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ this.temp = new Uint32Array(buf32.length);
+ for (var i = 0; i < buf32.length; i++) {
+ this.temp[i] = buf32[i];
+ }
+
+ //tempCanvasã«ä¹—ã›ã‚‹ç”»åƒã‚’作る
+ imageData = this.tempCanvasCtx.getImageData(x, y, width, height);
+ buf32 = new Uint32Array(imageData.data.buffer);
+ buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ for (var i = 0; i < buf32.length; i++) {
+ if (this.temp[i] >> 24) {
+ buf32[i] = this.temp[i] | 0xff000000;
+ } else {
+ buf32[i] = 0xffffffff;
+ }
+ }
+ imageData.data.set(buf8);
+ this.tempCanvasCtx.putImageData(imageData, x, y);
+};
+
+
+Neo.Painter.prototype.paste = function(x, y, width, height) {
+ var ctx = this.canvasCtx[this.current];
+// console.log(this.tempX, this.tempY);
+
+ var imageData = ctx.getImageData(x + this.tempX, y + this.tempY, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ for (var i = 0; i < buf32.length; i++) {
+ buf32[i] = this.temp[i];
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x + this.tempX, y + this.tempY);
+
+ this.temp = null;
+ this.tempX = 0;
+ this.tempY = 0;
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.turn = function(x, y, width, height) {
+ var ctx = this.canvasCtx[this.current];
+
+ // 傾ã‘ツールã®ãƒã‚°ã‚’å†ç¾ã™ã‚‹ãŸã‚一番上ã®ãƒ©ã‚¤ãƒ³ã§å¯¾è±¡é ˜åŸŸã‚’埋ã‚ã‚‹
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var temp = new Uint32Array(buf32.length);
+
+ var index = 0;
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ temp[index] = buf32[index];
+ if (index >= width) {
+ buf32[index] = buf32[index % width];
+ }
+ index++;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+
+ // 90度回転ã•ã›ã¦è²¼ã‚Šä»˜ã‘
+ imageData = ctx.getImageData(x, y, height, width);
+ buf32 = new Uint32Array(imageData.data.buffer);
+ buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ index = 0;
+ for (var j = height - 1; j >= 0; j--) {
+ for (var i = 0; i < width; i++) {
+ buf32[i * height + j] = temp[index++];
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.doFill = function(ctx, x, y, width, height, maskFunc) {
+ if (Math.round(x) != x) console.log("*");
+ if (Math.round(width) != width) console.log("*");
+ if (Math.round(height) != height) console.log("*");
+
+ var imageData = ctx.getImageData(x, y, width, height);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ var index = 0;
+
+ var r1 = this._currentColor[0];
+ var g1 = this._currentColor[1];
+ var b1 = this._currentColor[2];
+ var a1 = this.getAlpha(Neo.ALPHATYPE_FILL);
+
+ for (var j = 0; j < height; j++) {
+ for (var i = 0; i < width; i++) {
+ if (maskFunc && maskFunc.call(this, i, j, width, height)) {
+ //ãªãœã‹åŠ ç®—逆加算ã¯é©ç”¨ã•ã‚Œãªã„
+ if (this.maskType >= Neo.Painter.MASKTYPE_ADD ||
+ !this.isMasked(buf8, index)) {
+ var r0 = buf8[index + 0];
+ var g0 = buf8[index + 1];
+ var b0 = buf8[index + 2];
+ var a0 = buf8[index + 3] / 255.0;
+
+ var a = a0 + a1 - a0 * a1;
+
+ if (a > 0) {
+ var a1x = a1;
+ var ax = 1 + a0 * (1 - a1x);
+
+ var r = (r1 + r0 * a0 * (1 - a1x)) / ax;
+ var g = (g1 + g0 * a0 * (1 - a1x)) / ax;
+ var b = (b1 + b0 * a0 * (1 - a1x)) / ax
+
+ r = (r1 > r0) ? Math.ceil(r) : Math.floor(r);
+ g = (g1 > g0) ? Math.ceil(g) : Math.floor(g);
+ b = (b1 > b0) ? Math.ceil(b) : Math.floor(b);
+ }
+
+ var tmp = a * 255;
+ a = Math.ceil(tmp);
+
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+ }
+ }
+ index += 4;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, x, y);
+};
+
+Neo.Painter.prototype.rectFillMask = function(x, y, width, height) {
+ return true;
+};
+
+Neo.Painter.prototype.rectMask = function(x, y, width, height) {
+ var d = this.lineWidth;
+ return (x < d || x > width - 1 - d ||
+ y < d || y > height - 1 - d) ? true : false;
+};
+
+Neo.Painter.prototype.ellipseFillMask = function(x, y, width, height) {
+ var cx = (width - 1) / 2.0;
+ var cy = (height - 1) / 2.0;
+ x = (x - cx) / (cx + 1);
+ y = (y - cy) / (cy + 1);
+
+ return ((x * x) + (y * y) < 1) ? true : false;
+}
+
+Neo.Painter.prototype.ellipseMask = function(x, y, width, height) {
+ var d = this.lineWidth;
+ var cx = (width - 1) / 2.0;
+ var cy = (height - 1) / 2.0;
+
+ if (cx <= d || cy <= d) return this.ellipseFillMask(x, y, width, height);
+
+ var x2 = (x - cx) / (cx - d + 1);
+ var y2 = (y - cy) / (cy - d + 1);
+
+ x = (x - cx) / (cx + 1);
+ y = (y - cy) / (cy + 1);
+
+ if ((x * x) + (y * y) < 1) {
+ if ((x2 * x2) + (y2 * y2) >= 1) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+-----------------------------------------------------------------------
+*/
+
+Neo.Painter.prototype.getDestCanvasPosition = function(mx, my, isClip, isCenter) {
+ var mx = Math.floor(mx); //Math.round(mx);
+ var my = Math.floor(my); //Math.round(my);
+ if (isCenter) {
+ mx += 0.499;
+ my += 0.499;
+ }
+ var x = (mx - this.zoomX + this.destCanvas.width * 0.5 / this.zoom) * this.zoom;
+ var y = (my - this.zoomY + this.destCanvas.height * 0.5 / this.zoom) * this.zoom;
+
+ if (isClip) {
+ x = Math.max(Math.min(x, this.destCanvas.width), 0);
+ y = Math.max(Math.min(y, this.destCanvas.height), 0);
+ }
+ return {x:x, y:y};
+};
+
+Neo.Painter.prototype.isWidget = function(element) {
+ while (1) {
+ if (element == null ||
+ element.id == "canvas" ||
+ element.id == "container") break;
+
+ if (element.id == "tools" ||
+ element.className == "buttonOn" ||
+ element.className == "buttonOff" ||
+ element.className == "inputText") {
+ return true;
+ }
+ element = element.parentNode;
+ }
+ return false;
+};
+
+Neo.Painter.prototype.isContainer = function(element) {
+ while (1) {
+ if (element == null) break;
+ if (element.id == "container") return true;
+ element = element.parentNode;
+ }
+ return false;
+};
+
+Neo.Painter.prototype.cancelTool = function(e) {
+ if (this.tool) {
+ this.isMouseDown = false;
+ this.tool.upHandler(this);
+
+// switch (this.tool.type) {
+// case Neo.Painter.TOOLTYPE_HAND:
+// case Neo.Painter.TOOLTYPE_SLIDER:
+// this.isMouseDown = false;
+// this.tool.upHandler(this);
+// }
+ }
+};
+
+Neo.Painter.prototype.loadImage = function (filename) {
+ console.log("loadImage " + filename);
+ var img = new Image();
+ img.src = filename;
+ img.onload = function() {
+ var oe = Neo.painter;
+ oe.canvasCtx[0].drawImage(img, 0, 0);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight);
+ };
+};
+
+Neo.Painter.prototype.loadSession = function (filename) {
+ if (Neo.storage) {
+ var img0 = new Image();
+ img0.src = Neo.storage.getItem('layer0');
+ img0.onload = function() {
+ var img1 = new Image();
+ img1.src = Neo.storage.getItem('layer1');
+ img1.onload = function() {
+ var oe = Neo.painter;
+ oe.canvasCtx[0].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.canvasCtx[1].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.canvasCtx[0].drawImage(img0, 0, 0);
+ oe.canvasCtx[1].drawImage(img1, 0, 0);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight);
+ }
+ }
+ }
+};
+
+Neo.Painter.prototype.saveSession = function() {
+ if (Neo.storage) {
+ Neo.storage.setItem('timestamp', +(new Date()));
+ Neo.storage.setItem('layer0', this.canvas[0].toDataURL('image/png'));
+ Neo.storage.setItem('layer1', this.canvas[1].toDataURL('image/png'));
+ }
+};
+
+Neo.Painter.prototype.clearSession = function() {
+ if (Neo.storage) {
+ Neo.storage.removeItem('timestamp');
+ Neo.storage.removeItem('layer0');
+ Neo.storage.removeItem('layer1');
+ }
+};
+
+Neo.Painter.prototype.sortColor = function(r0, g0, b0) {
+ var min = (r0 < g0) ? ((r0 < b0) ? 0 : 2) : ((g0 < b0) ? 1 : 2);
+ var max = (r0 > g0) ? ((r0 > b0) ? 0 : 2) : ((g0 > b0) ? 1 : 2);
+ var mid = (min + max == 1) ? 2 : ((min + max == 2) ? 1 : 0);
+ return [min, mid, max];
+};
+
+Neo.Painter.prototype.doText = function(x, y, string, fontSize) {
+ //テキストæç”»
+ //æç”»ä½ç½®ãŒãšã‚Œã‚‹ã®ã§é©å½“ã«èª¿æ•´
+ var offset = parseInt(fontSize, 10);
+// y -= Math.round((5.0 + offset/8) / this.zoom);
+// x += Math.round(2.0 / this.zoom);
+
+ var ctx = this.tempCanvasCtx;
+ ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ ctx.save();
+ ctx.translate(x, y);
+// ctx.scale(1/this.zoom, 1/this.zoom);
+
+ var fontFamily = Neo.painter.inputText.style.fontFamily || "Arial";
+ ctx.font = fontSize + " " + fontFamily;
+
+ ctx.fillStyle = 0;
+ ctx.fillText(string, 0, 0);
+ ctx.restore();
+
+ // é©å½“ã«äºŒå€¤åŒ–
+ var c = this.getColor();
+ var r = c & 0xff;
+ var g = (c & 0xff00) >> 8;
+ var b = (c & 0xff0000) >> 16;
+ var a = Math.round(this.alpha * 255.0);
+
+ var imageData = ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var length = this.canvasWidth * this.canvasHeight;
+ var index = 0;
+ for (var i = 0; i < length; i++) {
+ if (buf8[index + 3] >= 0x60) {
+ buf8[index + 0] = r;
+ buf8[index + 1] = g;
+ buf8[index + 2] = b;
+ buf8[index + 3] = a;
+
+ } else {
+ buf8[index + 0] = 0;
+ buf8[index + 1] = 0;
+ buf8[index + 2] = 0;
+ buf8[index + 3] = 0;
+ }
+ index += 4;
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+
+ //キャンãƒã‚¹ã«è²¼ã‚Šä»˜ã‘
+ ctx = this.canvasCtx[this.current];
+ ctx.globalAlpha = 1.0;
+ ctx.drawImage(this.tempCanvas,
+ 0, 0, this.canvasWidth, this.canvasHeight,
+ 0, 0, this.canvasWidth, this.canvasHeight);
+
+ this.tempCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+};
+
+Neo.Painter.prototype.isUIPaused = function() {
+ if (this.drawType == Neo.Painter.DRAWTYPE_BEZIER) {
+ if (this.tool.step && this.tool.step > 0) {
+ return true;
+ }
+ }
+ return false;
+};
+
+Neo.Painter.prototype.getEmulationMode = function() {
+ return parseFloat(Neo.config.neo_emulation_mode || 2.22)
+};
+
+'use strict';
+
+Neo.ToolBase = function() {};
+
+Neo.ToolBase.prototype.startX;
+Neo.ToolBase.prototype.startY;
+Neo.ToolBase.prototype.init = function(oe) {}
+Neo.ToolBase.prototype.kill = function(oe) {}
+Neo.ToolBase.prototype.lineType = Neo.Painter.LINETYPE_NONE;
+
+Neo.ToolBase.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+};
+
+Neo.ToolBase.prototype.upHandler = function(oe) {
+};
+
+Neo.ToolBase.prototype.moveHandler = function(oe) {
+};
+
+Neo.ToolBase.prototype.transformForZoom = function(oe) {
+ var ctx = oe.destCanvasCtx;
+ ctx.translate(oe.canvasWidth * 0.5, oe.canvasHeight * 0.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+};
+
+Neo.ToolBase.prototype.getType = function() {
+ return this.type;
+};
+
+Neo.ToolBase.prototype.getToolButton = function() {
+ switch (this.type) {
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TEXT:
+ return Neo.penTip;
+
+ case Neo.Painter.TOOLTYPE_TONE:
+ case Neo.Painter.TOOLTYPE_BLUR:
+ case Neo.Painter.TOOLTYPE_DODGE:
+ case Neo.Painter.TOOLTYPE_BURN:
+ return Neo.pen2Tip;
+
+ case Neo.Painter.TOOLTYPE_RECT:
+ case Neo.Painter.TOOLTYPE_RECTFILL:
+ case Neo.Painter.TOOLTYPE_ELLIPSE:
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:
+ return Neo.effectTip;
+
+ case Neo.Painter.TOOLTYPE_COPY:
+ case Neo.Painter.TOOLTYPE_MERGE:
+ case Neo.Painter.TOOLTYPE_BLURRECT:
+ case Neo.Painter.TOOLTYPE_FLIP_H:
+ case Neo.Painter.TOOLTYPE_FLIP_V:
+ case Neo.Painter.TOOLTYPE_TURN:
+ return Neo.effect2Tip;
+
+ case Neo.Painter.TOOLTYPE_ERASER:
+ case Neo.Painter.TOOLTYPE_ERASEALL:
+ case Neo.Painter.TOOLTYPE_ERASERECT:
+ return Neo.eraserTip;
+
+ case Neo.Painter.TOOLTYPE_FILL:
+ return Neo.fillButton;
+ }
+ return null;
+};
+
+Neo.ToolBase.prototype.getReserve = function() {
+ switch (this.type) {
+ case Neo.Painter.TOOLTYPE_ERASER:
+ return Neo.reserveEraser;
+
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TONE:
+ case Neo.Painter.TOOLTYPE_ERASERECT:
+ case Neo.Painter.TOOLTYPE_ERASEALL:
+ case Neo.Painter.TOOLTYPE_COPY:
+ case Neo.Painter.TOOLTYPE_MERGE:
+ case Neo.Painter.TOOLTYPE_FIP_H:
+ case Neo.Painter.TOOLTYPE_FIP_V:
+
+ case Neo.Painter.TOOLTYPE_DODGE:
+ case Neo.Painter.TOOLTYPE_BURN:
+ case Neo.Painter.TOOLTYPE_BLUR:
+ case Neo.Painter.TOOLTYPE_BLURRECT:
+
+ case Neo.Painter.TOOLTYPE_TEXT:
+ case Neo.Painter.TOOLTYPE_TURN:
+ case Neo.Painter.TOOLTYPE_RECT:
+ case Neo.Painter.TOOLTYPE_RECTFILL:
+ case Neo.Painter.TOOLTYPE_ELLIPSE:
+ case Neo.Painter.TOOLTYPE_ELLIPSEFILL:
+ return Neo.reservePen;
+
+ }
+ return null;
+};
+
+Neo.ToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.updateUI();
+ }
+};
+
+Neo.ToolBase.prototype.saveStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ reserve.size = Neo.painter.lineWidth;
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ DrawToolBase(æ画ツールã®ãƒ™ãƒ¼ã‚¹ã‚¯ãƒ©ã‚¹ï¼‰
+ -------------------------------------------------------------------------
+*/
+
+Neo.DrawToolBase = function() {};
+Neo.DrawToolBase.prototype = new Neo.ToolBase();
+Neo.DrawToolBase.prototype.isUpMove = false;
+Neo.DrawToolBase.prototype.step = 0;
+
+Neo.DrawToolBase.prototype.init = function() {
+ this.step = 0;
+ this.isUpMove = true;
+};
+
+Neo.DrawToolBase.prototype.downHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandDownHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineDownHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierDownHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.upHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandUpHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineUpHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierUpHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.moveHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierMoveHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.upMoveHandler = function(oe) {
+ switch (oe.drawType) {
+ case Neo.Painter.DRAWTYPE_FREEHAND:
+ this.freeHandUpMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_LINE:
+ this.lineUpMoveHandler(oe); break;
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierUpMoveHandler(oe); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.keyDownHandler = function(e) {
+ switch (Neo.painter.drawType) {
+ case Neo.Painter.DRAWTYPE_BEZIER:
+ this.bezierKeyDownHandler(e); break;
+ }
+};
+
+Neo.DrawToolBase.prototype.rollOverHandler= function(oe) {};
+Neo.DrawToolBase.prototype.rollOutHandler= function(oe) {
+ if (!oe.isMouseDown && !oe.isMouseDownRight){
+ oe.tempCanvasCtx.clearRect(0,0,oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.DrawToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+};
+
+
+/* FreeHand (手書ã) */
+
+Neo.DrawToolBase.prototype.freeHandDownHandler = function(oe) {
+ //Register undo first;
+ oe._pushUndo();
+
+ oe.prepareDrawing();
+ this.isUpMove = false;
+ var ctx = oe.canvasCtx[oe.current];
+ if (oe.alpha >= 1 || this.lineType != Neo.Painter.LINETYPE_BRUSH) {
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ oe.drawLine(ctx, x0, y0, x0, y0, this.lineType);
+ }
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+ if (oe.alpha >= 1) {
+ var r = Math.ceil(oe.lineWidth / 2);
+ var rect = oe.getBound(oe.mouseX, oe.mouseY, oe.mouseX, oe.mouseY, r);
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ }
+};
+
+Neo.DrawToolBase.prototype.freeHandUpHandler = function(oe) {
+ oe.tempCanvasCtx.clearRect(0,0,oe.canvasWidth, oe.canvasHeight);
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+ // oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ // this.drawCursor(oe);
+ oe.prevLine = null;
+};
+
+Neo.DrawToolBase.prototype.freeHandMoveHandler = function(oe) {
+ var ctx = oe.canvasCtx[oe.current];
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ var x1 = Math.floor(oe.prevMouseX);
+ var y1 = Math.floor(oe.prevMouseY);
+ oe.drawLine(ctx, x0, y0, x1, y1, this.lineType);
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+
+ var r = Math.ceil(oe.lineWidth / 2);
+ var rect = oe.getBound(oe.mouseX, oe.mouseY, oe.prevMouseX, oe.prevMouseY, r);
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+};
+
+Neo.DrawToolBase.prototype.freeHandUpMoveHandler = function(oe) {
+ this.isUpMove = true;
+
+ if (oe.cursorRect) {
+ var rect = oe.cursorRect;
+ oe.updateDestCanvas(rect[0], rect[1], rect[2], rect[3], true);
+ oe.cursorRect = null;
+ }
+ this.drawCursor(oe);
+};
+
+Neo.DrawToolBase.prototype.drawCursor = function(oe) {
+ if (oe.lineWidth <= 8) return;
+ var mx = oe.mouseX;
+ var my = oe.mouseY;
+ var d = oe.lineWidth;
+
+ var x = (mx - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y = (my - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ var r = d * 0.5 * oe.zoom;
+
+ if (!(x > -r &&
+ y > -r &&
+ x < oe.destCanvas.width + r &&
+ y < oe.destCanvas.height + r)) return;
+
+ var ctx = oe.destCanvasCtx;
+ ctx.save();
+ this.transformForZoom(oe)
+
+ var c = (this.type == Neo.Painter.TOOLTYPE_ERASER) ? 0x0000ff : 0xffff7f;
+ oe.drawXOREllipse(ctx, x-r, y-r, r*2, r*2, false, c);
+
+ ctx.restore();
+ oe.cursorRect = oe.getBound(mx, my, mx, my, Math.ceil(d / 2));
+}
+
+
+/* Line (ç›´ç·š) */
+
+Neo.DrawToolBase.prototype.lineDownHandler = function(oe) {
+ this.isUpMove = false;
+ this.startX = Math.floor(oe.mouseX);
+ this.startY = Math.floor(oe.mouseY);
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+};
+
+Neo.DrawToolBase.prototype.lineUpHandler = function(oe) {
+ if (this.isUpMove == false) {
+ this.isUpMove = true;
+
+ oe._pushUndo();
+ oe.prepareDrawing();
+ var ctx = oe.canvasCtx[oe.current];
+ var x0 = Math.floor(oe.mouseX);
+ var y0 = Math.floor(oe.mouseY);
+ oe.drawLine(ctx, x0, y0, this.startX, this.startY, this.lineType);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.DrawToolBase.prototype.lineMoveHandler = function(oe) {
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ this.drawLineCursor(oe);
+};
+
+Neo.DrawToolBase.prototype.lineUpMoveHandler = function(oe) {
+};
+
+Neo.DrawToolBase.prototype.drawLineCursor = function(oe, mx, my) {
+ if (!mx) mx = Math.floor(oe.mouseX);
+ if (!my) my = Math.floor(oe.mouseY);
+ var nx = this.startX;
+ var ny = this.startY;
+ var ctx = oe.destCanvasCtx;
+ ctx.save();
+ this.transformForZoom(oe)
+
+ var x0 = (mx +.499 - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y0 = (my +.499 - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ var x1 = (nx +.499 - oe.zoomX + oe.destCanvas.width * 0.5 / oe.zoom) * oe.zoom;
+ var y1 = (ny +.499 - oe.zoomY + oe.destCanvas.height * 0.5 / oe.zoom) * oe.zoom;
+ oe.drawXORLine(ctx, x0, y0, x1, y1);
+
+ ctx.restore();
+};
+
+
+/* Bezier (BZ曲線) */
+
+Neo.DrawToolBase.prototype.bezierDownHandler = function(oe) {
+ this.isUpMove = false;
+
+ if (this.step == 0) {
+ this.startX = this.x0 = Math.floor(oe.mouseX);
+ this.startY = this.y0 = Math.floor(oe.mouseY);
+ }
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+};
+
+Neo.DrawToolBase.prototype.bezierUpHandler = function(oe) {
+ if (this.isUpMove == false) {
+ this.isUpMove = true;
+ }
+
+ this.step++;
+ switch (this.step) {
+ case 1:
+ oe.prepareDrawing();
+ this.x3 = Math.floor(oe.mouseX);
+ this.y3 = Math.floor(oe.mouseY);
+ break;
+
+ case 2:
+ this.x1 = Math.floor(oe.mouseX);
+ this.y1 = Math.floor(oe.mouseY);
+ break;
+
+ case 3:
+ this.x2 = Math.floor(oe.mouseX);
+ this.y2 = Math.floor(oe.mouseY);
+
+ oe._pushUndo();
+ oe.drawBezier(oe.canvasCtx[oe.current],
+ this.x0, this.y0, this.x1, this.y1,
+ this.x2, this.y2, this.x3, this.y3, this.lineType);
+
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ this.step = 0;
+ break;
+
+ default:
+ this.step = 0;
+ break;
+ }
+};
+
+Neo.DrawToolBase.prototype.bezierMoveHandler = function(oe) {
+ switch (this.step) {
+ case 0:
+ if (!this.isUpMove) {
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawLineCursor(oe);
+ }
+ break;
+ case 1:
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawBezierCursor1(oe);
+ break;
+
+ case 2:
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, false);
+ this.drawBezierCursor2(oe);
+ break;
+ }
+};
+
+Neo.DrawToolBase.prototype.bezierUpMoveHandler = function(oe) {
+ this.bezierMoveHandler(oe);
+};
+
+Neo.DrawToolBase.prototype.bezierKeyDownHandler = function(e) {
+ if (e.keyCode == 27) { //Escã§ã‚­ãƒ£ãƒ³ã‚»ãƒ«
+ this.step = 0;
+
+ var oe = Neo.painter;
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+
+Neo.DrawToolBase.prototype.drawBezierCursor1 = function(oe) {
+ var ctx = oe.destCanvasCtx;
+ // var x = oe.mouseX; //Math.floor(oe.mouseX);
+ // var y = oe.mouseY; //Math.floor(oe.mouseY);
+ var stab = oe.getStabilized();
+ var x = Math.floor(stab[0]);
+ var y = Math.floor(stab[1]);
+ var p = oe.getDestCanvasPosition(x, y, false, true);
+ var p0 = oe.getDestCanvasPosition(this.x0, this.y0, false, true);
+ var p3 = oe.getDestCanvasPosition(this.x3, this.y3, false, true);
+
+ // handle
+ oe.drawXORLine(ctx, p0.x, p0.y, p.x, p.y);
+ oe.drawXOREllipse(ctx, p.x - 4, p.y - 4, 8, 8);
+ oe.drawXOREllipse(ctx, p0.x - 4, p0.y - 4, 8, 8);
+
+ // preview
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.drawBezier(oe.tempCanvasCtx,
+ this.x0, this.y0,
+ x, y,
+ x, y,
+ this.x3, this.y3, this.lineType);
+
+ ctx.save();
+ ctx.translate(oe.destCanvas.width*.5, oe.destCanvas.height*.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+ ctx.drawImage(oe.tempCanvas,
+ 0, 0, oe.canvasWidth, oe.canvasHeight,
+ 0, 0, oe.canvasWidth, oe.canvasHeight);
+
+ ctx.restore();
+};
+
+Neo.DrawToolBase.prototype.drawBezierCursor2 = function(oe) {
+ var ctx = oe.destCanvasCtx;
+ // var x = oe.mouseX; //Math.floor(oe.mouseX);
+ // var y = oe.mouseY; //Math.floor(oe.mouseY);
+ var stab = oe.getStabilized();
+ var x = Math.floor(stab[0]);
+ var y = Math.floor(stab[1]);
+ var p = oe.getDestCanvasPosition(oe.mouseX, oe.mouseY, false, true);
+ var p0 = oe.getDestCanvasPosition(this.x0, this.y0, false, true);
+ var p1 = oe.getDestCanvasPosition(this.x1, this.y1, false, true);
+ var p3 = oe.getDestCanvasPosition(this.x3, this.y3, false, true);
+
+ // handle
+ oe.drawXORLine(ctx, p3.x, p3.y, p.x, p.y);
+ oe.drawXOREllipse(ctx, p.x - 4, p.y - 4, 8, 8);
+ oe.drawXORLine(ctx, p0.x, p0.y, p1.x, p1.y);
+ oe.drawXOREllipse(ctx, p1.x - 4, p1.y - 4, 8, 8);
+ oe.drawXOREllipse(ctx, p0.x - 4, p0.y - 4, 8, 8);
+
+ // preview
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.drawBezier(oe.tempCanvasCtx,
+ this.x0, this.y0,
+ this.x1, this.y1,
+ x, y,
+ this.x3, this.y3, this.lineType);
+
+ ctx.save();
+ ctx.translate(oe.destCanvas.width*.5, oe.destCanvas.height*.5);
+ ctx.scale(oe.zoom, oe.zoom);
+ ctx.translate(-oe.zoomX, -oe.zoomY);
+ ctx.drawImage(oe.tempCanvas,
+ 0, 0, oe.canvasWidth, oe.canvasHeight,
+ 0, 0, oe.canvasWidth, oe.canvasHeight);
+ ctx.restore();
+};
+
+/*
+ -------------------------------------------------------------------------
+ Pen(鉛筆)
+ -------------------------------------------------------------------------
+*/
+
+Neo.PenTool = function() {};
+Neo.PenTool.prototype = new Neo.DrawToolBase();
+Neo.PenTool.prototype.type = Neo.Painter.TOOLTYPE_PEN;
+Neo.PenTool.prototype.lineType = Neo.Painter.LINETYPE_PEN;
+
+Neo.PenTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+}
+
+/*
+ -------------------------------------------------------------------------
+ Brush(水彩)
+ -------------------------------------------------------------------------
+*/
+
+Neo.BrushTool = function() {};
+Neo.BrushTool.prototype = new Neo.DrawToolBase();
+Neo.BrushTool.prototype.type = Neo.Painter.TOOLTYPE_BRUSH;
+Neo.BrushTool.prototype.lineType = Neo.Painter.LINETYPE_BRUSH;
+
+Neo.BrushTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = this.getAlpha();
+ Neo.updateUI();
+ }
+};
+
+Neo.BrushTool.prototype.getAlpha = function() {
+ var alpha = 241 - Math.floor(Neo.painter.lineWidth / 2) * 6;
+ return alpha / 255.0;
+};
+
+/*
+ -------------------------------------------------------------------------
+ Tone(トーン)
+ -------------------------------------------------------------------------
+*/
+
+Neo.ToneTool = function() {};
+Neo.ToneTool.prototype = new Neo.DrawToolBase();
+Neo.ToneTool.prototype.type = Neo.Painter.TOOLTYPE_TONE;
+Neo.ToneTool.prototype.lineType = Neo.Painter.LINETYPE_TONE;
+
+Neo.ToneTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 23 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Eraser(消ã—ペン)
+ -------------------------------------------------------------------------
+*/
+
+Neo.EraserTool = function() {};
+Neo.EraserTool.prototype = new Neo.DrawToolBase();
+Neo.EraserTool.prototype.type = Neo.Painter.TOOLTYPE_ERASER;
+Neo.EraserTool.prototype.lineType = Neo.Painter.LINETYPE_ERASER;
+
+
+/*
+ -------------------------------------------------------------------------
+ Blur(ã¼ã‹ã—)
+ -------------------------------------------------------------------------
+*/
+
+Neo.BlurTool = function() {};
+Neo.BlurTool.prototype = new Neo.DrawToolBase();
+Neo.BlurTool.prototype.type = Neo.Painter.TOOLTYPE_BLUR;
+Neo.BlurTool.prototype.lineType = Neo.Painter.LINETYPE_BLUR;
+
+Neo.BlurTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Dodge(覆ã„焼ã)
+ -------------------------------------------------------------------------
+*/
+
+Neo.DodgeTool = function() {};
+Neo.DodgeTool.prototype = new Neo.DrawToolBase();
+Neo.DodgeTool.prototype.type = Neo.Painter.TOOLTYPE_DODGE;
+Neo.DodgeTool.prototype.lineType = Neo.Painter.LINETYPE_DODGE;
+
+Neo.DodgeTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Burn(焼ãè¾¼ã¿ï¼‰
+ -------------------------------------------------------------------------
+*/
+
+Neo.BurnTool = function() {};
+Neo.BurnTool.prototype = new Neo.DrawToolBase();
+Neo.BurnTool.prototype.type = Neo.Painter.TOOLTYPE_BURN;
+Neo.BurnTool.prototype.lineType = Neo.Painter.LINETYPE_BURN;
+
+Neo.BurnTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 128 / 255.0;
+ Neo.updateUI();
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Hand(スクロール)
+ -------------------------------------------------------------------------
+*/
+
+Neo.HandTool = function() {};
+Neo.HandTool.prototype = new Neo.ToolBase();
+Neo.HandTool.prototype.type = Neo.Painter.TOOLTYPE_HAND;
+Neo.HandTool.prototype.isUpMove = false;
+Neo.HandTool.prototype.reverse = false;
+
+Neo.HandTool.prototype.downHandler = function(oe) {
+ oe.tempCanvasCtx.clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+
+ this.isDrag = true;
+ this.startX = oe.rawMouseX;
+ this.startY = oe.rawMouseY;
+};
+
+Neo.HandTool.prototype.upHandler = function(oe) {
+ this.isDrag = false;
+ oe.popTool();
+};
+
+Neo.HandTool.prototype.moveHandler = function(oe) {
+ if (this.isDrag) {
+ var dx = this.startX - oe.rawMouseX;
+ var dy = this.startY - oe.rawMouseY;
+
+ var ax = oe.destCanvas.width / (oe.canvasWidth * oe.zoom);
+ var ay = oe.destCanvas.height / (oe.canvasHeight * oe.zoom);
+ var barWidth = oe.destCanvas.width * ax;
+ var barHeight = oe.destCanvas.height * ay;
+ var scrollWidthInScreen = oe.destCanvas.width - barWidth - 2;
+ var scrollHeightInScreen = oe.destCanvas.height - barHeight - 2;
+
+ dx *= oe.scrollWidth / scrollWidthInScreen;
+ dy *= oe.scrollHeight / scrollHeightInScreen;
+
+ if (this.reverse) {
+ dx *= -1;
+ dy *= -1;
+ }
+
+ oe.setZoomPosition(oe.zoomX - dx, oe.zoomY - dy);
+
+ this.startX = oe.rawMouseX;
+ this.startY = oe.rawMouseY;
+ }
+};
+
+Neo.HandTool.prototype.upMoveHandler = function(oe) {}
+Neo.HandTool.prototype.rollOverHandler= function(oe) {}
+Neo.HandTool.prototype.rollOutHandler= function(oe) {};
+
+/*
+ -------------------------------------------------------------------------
+ Slider(色やサイズã®ã‚¹ãƒ©ã‚¤ãƒ€ã‚’æ“作ã—ã¦ã„る時)
+ -------------------------------------------------------------------------
+*/
+
+Neo.SliderTool = function() {};
+Neo.SliderTool.prototype = new Neo.ToolBase();
+Neo.SliderTool.prototype.type = Neo.Painter.TOOLTYPE_SLIDER;
+Neo.SliderTool.prototype.isUpMove = false;
+Neo.SliderTool.prototype.alt = false;
+
+Neo.SliderTool.prototype.downHandler = function(oe) {
+ if (!oe.isShiftDown) this.isDrag = true;
+
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].downHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+};
+
+Neo.SliderTool.prototype.upHandler = function(oe) {
+ this.isDrag = false;
+ oe.popTool();
+
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].upHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+};
+
+Neo.SliderTool.prototype.moveHandler = function(oe) {
+ if (this.isDrag) {
+ var rect = this.target.getBoundingClientRect();
+ var sliderType = (this.alt) ? Neo.SLIDERTYPE_SIZE : this.target['data-slider'];
+ Neo.sliders[sliderType].moveHandler(oe.rawMouseX - rect.left,
+ oe.rawMouseY - rect.top);
+ }
+};
+
+Neo.SliderTool.prototype.upMoveHandler = function(oe) {}
+Neo.SliderTool.prototype.rollOverHandler= function(oe) {}
+Neo.SliderTool.prototype.rollOutHandler= function(oe) {}
+
+/*
+ -------------------------------------------------------------------------
+ Fill(塗り潰ã—)
+ -------------------------------------------------------------------------
+*/
+
+Neo.FillTool = function() {};
+Neo.FillTool.prototype = new Neo.ToolBase();
+Neo.FillTool.prototype.type = Neo.Painter.TOOLTYPE_FILL;
+Neo.FillTool.prototype.isUpMove = false;
+
+Neo.FillTool.prototype.downHandler = function(oe) {
+ var x = Math.floor(oe.mouseX);
+ var y = Math.floor(oe.mouseY);
+ oe._pushUndo();
+ oe.fill(x, y, oe.canvasCtx[oe.current]);
+};
+
+Neo.FillTool.prototype.upHandler = function(oe) {
+};
+
+Neo.FillTool.prototype.moveHandler = function(oe) {
+};
+
+Neo.FillTool.prototype.rollOutHandler= function(oe) {};
+Neo.FillTool.prototype.upMoveHandler = function(oe) {}
+Neo.FillTool.prototype.rollOverHandler= function(oe) {}
+
+
+/*
+ -------------------------------------------------------------------------
+ EraseAll(全消ã—)
+ -------------------------------------------------------------------------
+*/
+
+Neo.EraseAllTool = function() {};
+Neo.EraseAllTool.prototype = new Neo.ToolBase();
+Neo.EraseAllTool.prototype.type = Neo.Painter.TOOLTYPE_ERASEALL;
+Neo.EraseAllTool.prototype.isUpMove = false;
+
+Neo.EraseAllTool.prototype.downHandler = function(oe) {
+ oe._pushUndo();
+
+ oe.prepareDrawing();
+ oe.canvasCtx[oe.current].clearRect(0, 0, oe.canvasWidth, oe.canvasHeight);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+Neo.EraseAllTool.prototype.upHandler = function(oe) {
+};
+
+Neo.EraseAllTool.prototype.moveHandler = function(oe) {
+};
+
+Neo.EraseAllTool.prototype.rollOutHandler= function(oe) {};
+Neo.EraseAllTool.prototype.upMoveHandler = function(oe) {};
+Neo.EraseAllTool.prototype.rollOverHandler= function(oe) {};
+
+
+/*
+ -------------------------------------------------------------------------
+ EffectToolBase(エフェックトツールã®ãƒ™ãƒ¼ã‚¹ã‚¯ãƒ©ã‚¹ï¼‰
+ -------------------------------------------------------------------------
+*/
+
+Neo.EffectToolBase = function() {};
+Neo.EffectToolBase.prototype = new Neo.ToolBase();
+Neo.EffectToolBase.prototype.isUpMove = false;
+
+Neo.EffectToolBase.prototype.downHandler = function(oe) {
+ this.isUpMove = false;
+
+ this.startX = this.endX = oe.clipMouseX;
+ this.startY = this.endY = oe.clipMouseY;
+};
+
+Neo.EffectToolBase.prototype.upHandler = function(oe) {
+ if (this.isUpMove) return;
+ this.isUpMove = true;
+
+ this.startX = Math.floor(this.startX);
+ this.startY = Math.floor(this.startY);
+ this.endX = Math.floor(this.endX);
+ this.endY = Math.floor(this.endY);
+
+ var x = (this.startX < this.endX) ? this.startX : this.endX;
+ var y = (this.startY < this.endY) ? this.startY : this.endY;
+ var width = Math.abs(this.startX - this.endX) + 1;
+ var height = Math.abs(this.startY - this.endY) + 1;
+ var ctx = oe.canvasCtx[oe.current];
+
+ if (x < 0) x = 0;
+ if (y < 0) y = 0;
+ if (x + width > oe.canvasWidth) width = oe.canvasWidth - x;
+ if (y + height > oe.canvasHeight) height = oe.canvasHeight - y;
+
+ if (width > 0 && height > 0) {
+ oe._pushUndo();
+ oe.prepareDrawing();
+ this.doEffect(oe, x, y, width, height);
+ }
+
+ if (oe.tool.type != Neo.Painter.TOOLTYPE_PASTE) {
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+Neo.EffectToolBase.prototype.moveHandler = function(oe) {
+ this.endX = oe.clipMouseX;
+ this.endY = oe.clipMouseY;
+
+ oe.updateDestCanvas(0,0,oe.canvasWidth, oe.canvasHeight, true);
+ this.drawCursor(oe);
+};
+
+Neo.EffectToolBase.prototype.rollOutHandler= function(oe) {};
+Neo.EffectToolBase.prototype.upMoveHandler = function(oe) {};
+Neo.EffectToolBase.prototype.rollOverHandler= function(oe) {};
+
+Neo.EffectToolBase.prototype.drawCursor = function(oe) {
+ var ctx = oe.destCanvasCtx;
+
+ ctx.save();
+ this.transformForZoom(oe);
+
+ var start = oe.getDestCanvasPosition(this.startX, this.startY, true);
+ var end = oe.getDestCanvasPosition(this.endX, this.endY, true);
+
+ var x = (start.x < end.x) ? start.x : end.x;
+ var y = (start.y < end.y) ? start.y : end.y;
+ var width = Math.abs(start.x - end.x) + oe.zoom;
+ var height = Math.abs(start.y - end.y) + oe.zoom;
+
+ if (this.isEllipse) {
+ oe.drawXOREllipse(ctx, x, y, width, height, this.isFill);
+
+ } else {
+ oe.drawXORRect(ctx, x, y, width, height, this.isFill);
+ }
+ ctx.restore();
+};
+
+Neo.EffectToolBase.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = this.defaultAlpha || 1.0;
+ Neo.updateUI();
+ };
+};
+
+/*
+ -------------------------------------------------------------------------
+ EraseRect(消ã—四角)
+ -------------------------------------------------------------------------
+*/
+
+Neo.EraseRectTool = function() {};
+Neo.EraseRectTool.prototype = new Neo.EffectToolBase();
+Neo.EraseRectTool.prototype.type = Neo.Painter.TOOLTYPE_ERASERECT;
+
+Neo.EraseRectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.eraseRect(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ FlipH(左å³å転)
+ -------------------------------------------------------------------------
+*/
+
+Neo.FlipHTool = function() {};
+Neo.FlipHTool.prototype = new Neo.EffectToolBase();
+Neo.FlipHTool.prototype.type = Neo.Painter.TOOLTYPE_FLIP_H;
+
+Neo.FlipHTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.flipH(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ FlipV(上下å転)
+ -------------------------------------------------------------------------
+*/
+
+Neo.FlipVTool = function() {};
+Neo.FlipVTool.prototype = new Neo.EffectToolBase();
+Neo.FlipVTool.prototype.type = Neo.Painter.TOOLTYPE_FLIP_V;
+
+Neo.FlipVTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.flipV(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ DodgeRect(角å–り)
+ -------------------------------------------------------------------------
+*/
+
+Neo.BlurRectTool = function() {};
+Neo.BlurRectTool.prototype = new Neo.EffectToolBase();
+Neo.BlurRectTool.prototype.type = Neo.Painter.TOOLTYPE_BLURRECT;
+
+Neo.BlurRectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.blurRect(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+Neo.BlurRectTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 0.5;
+ Neo.updateUI();
+ };
+}
+
+/*
+ -------------------------------------------------------------------------
+ Turn(傾ã‘)
+ -------------------------------------------------------------------------
+*/
+
+Neo.TurnTool = function() {};
+Neo.TurnTool.prototype = new Neo.EffectToolBase();
+Neo.TurnTool.prototype.type = Neo.Painter.TOOLTYPE_TURN;
+
+Neo.TurnTool.prototype.upHandler = function(oe) {
+ this.isUpMove = true;
+
+ this.startX = Math.floor(this.startX);
+ this.startY = Math.floor(this.startY);
+ this.endX = Math.floor(this.endX);
+ this.endY = Math.floor(this.endY);
+
+ var x = (this.startX < this.endX) ? this.startX : this.endX;
+ var y = (this.startY < this.endY) ? this.startY : this.endY;
+ var width = Math.abs(this.startX - this.endX) + 1;
+ var height = Math.abs(this.startY - this.endY) + 1;
+
+ if (width > 0 && height > 0) {
+ oe._pushUndo();
+ oe.turn(x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Merge(レイヤーçµåˆï¼‰
+ -------------------------------------------------------------------------
+*/
+
+Neo.MergeTool = function() {};
+Neo.MergeTool.prototype = new Neo.EffectToolBase();
+Neo.MergeTool.prototype.type = Neo.Painter.TOOLTYPE_MERGE;
+
+Neo.MergeTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.merge(ctx, x, y, width, height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ Copy(コピー)
+ -------------------------------------------------------------------------
+*/
+
+Neo.CopyTool = function() {};
+Neo.CopyTool.prototype = new Neo.EffectToolBase();
+Neo.CopyTool.prototype.type = Neo.Painter.TOOLTYPE_COPY;
+
+Neo.CopyTool.prototype.doEffect = function(oe, x, y, width, height) {
+ oe.copy(x, y, width, height);
+ oe.setToolByType(Neo.Painter.TOOLTYPE_PASTE);
+ oe.tool.x = x;
+ oe.tool.y = y;
+ oe.tool.width = width;
+ oe.tool.height = height;
+};
+
+/*
+ -------------------------------------------------------------------------
+ Paste(ペースト)
+ -------------------------------------------------------------------------
+*/
+
+Neo.PasteTool = function() {};
+Neo.PasteTool.prototype = new Neo.ToolBase();
+Neo.PasteTool.prototype.type = Neo.Painter.TOOLTYPE_PASTE;
+
+Neo.PasteTool.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+ this.drawCursor(oe);
+};
+
+Neo.PasteTool.prototype.upHandler = function(oe) {
+ oe._pushUndo();
+
+ oe.paste(this.x, this.y, this.width, this.height);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ oe.setToolByType(Neo.Painter.TOOLTYPE_COPY);
+};
+
+Neo.PasteTool.prototype.moveHandler = function(oe) {
+ var dx = Math.floor(oe.mouseX - this.startX);
+ var dy = Math.floor(oe.mouseY - this.startY);
+ oe.tempX = dx;
+ oe.tempY = dy;
+
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ // this.drawCursor(oe);
+};
+
+Neo.PasteTool.prototype.keyDownHandler = function(e) {
+ if (e.keyCode == 27) { //Escã§ã‚­ãƒ£ãƒ³ã‚»ãƒ«
+ var oe = Neo.painter;
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+ oe.setToolByType(Neo.Painter.TOOLTYPE_COPY);
+ }
+};
+
+Neo.PasteTool.prototype.drawCursor = function(oe) {
+ var ctx = oe.destCanvasCtx;
+
+ ctx.save();
+ this.transformForZoom(oe);
+
+ var start = oe.getDestCanvasPosition(this.x, this.y, true);
+ var end = oe.getDestCanvasPosition(this.x + this.width, this.y + this.height, true);
+
+ var x = start.x + oe.tempX * oe.zoom;
+ var y = start.y + oe.tempY * oe.zoom;
+ var width = Math.abs(start.x - end.x);
+ var height = Math.abs(start.y - end.y);
+ oe.drawXORRect(ctx, x, y, width, height);
+ ctx.restore();
+};
+
+/*
+ -------------------------------------------------------------------------
+ Rect(線四角)
+ -------------------------------------------------------------------------
+*/
+
+Neo.RectTool = function() {};
+Neo.RectTool.prototype = new Neo.EffectToolBase();
+Neo.RectTool.prototype.type = Neo.Painter.TOOLTYPE_RECT;
+
+Neo.RectTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.rectMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ RectFill(四角)
+ -------------------------------------------------------------------------
+*/
+
+Neo.RectFillTool = function() {};
+Neo.RectFillTool.prototype = new Neo.EffectToolBase();
+Neo.RectFillTool.prototype.type = Neo.Painter.TOOLTYPE_RECTFILL;
+
+Neo.RectFillTool.prototype.isFill = true;
+Neo.RectFillTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.rectFillMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ Ellipse(線楕円)
+ -------------------------------------------------------------------------
+*/
+
+Neo.EllipseTool = function() {};
+Neo.EllipseTool.prototype = new Neo.EffectToolBase();
+Neo.EllipseTool.prototype.type = Neo.Painter.TOOLTYPE_ELLIPSE;
+Neo.EllipseTool.prototype.isEllipse = true;
+Neo.EllipseTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.ellipseMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ EllipseFill(楕円)
+ -------------------------------------------------------------------------
+*/
+
+Neo.EllipseFillTool = function() {};
+Neo.EllipseFillTool.prototype = new Neo.EffectToolBase();
+Neo.EllipseFillTool.prototype.type = Neo.Painter.TOOLTYPE_ELLIPSEFILL;
+Neo.EllipseFillTool.prototype.isEllipse = true;
+Neo.EllipseFillTool.prototype.isFill = true;
+Neo.EllipseFillTool.prototype.doEffect = function(oe, x, y, width, height) {
+ var ctx = oe.canvasCtx[oe.current];
+ oe.doFill(ctx, x, y, width, height, oe.ellipseFillMask);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+};
+
+/*
+ -------------------------------------------------------------------------
+ Text(テキスト)
+ -------------------------------------------------------------------------
+*/
+
+Neo.TextTool = function() {};
+Neo.TextTool.prototype = new Neo.ToolBase();
+Neo.TextTool.prototype.type = Neo.Painter.TOOLTYPE_TEXT;
+Neo.TextTool.prototype.isUpMove = false;
+
+Neo.TextTool.prototype.downHandler = function(oe) {
+ this.startX = oe.mouseX;
+ this.startY = oe.mouseY;
+
+ if (Neo.painter.inputText) {
+ Neo.painter.updateInputText();
+
+ var rect = oe.container.getBoundingClientRect();
+ var text = Neo.painter.inputText;
+ var x = oe.rawMouseX - rect.left - 5;
+ var y = oe.rawMouseY - rect.top - 5;
+
+ text.style.left = x + "px";
+ text.style.top = y + "px";
+ text.style.display = "block";
+ text.focus();
+ }
+};
+
+Neo.TextTool.prototype.upHandler = function(oe) {
+};
+
+Neo.TextTool.prototype.moveHandler = function(oe) {};
+Neo.TextTool.prototype.upMoveHandler = function(oe) {};
+Neo.TextTool.prototype.rollOverHandler= function(oe) {};
+Neo.TextTool.prototype.rollOutHandler= function(oe) {};
+
+Neo.TextTool.prototype.keyDownHandler = function(e) {
+ if (e.keyCode == 13) { // Returnã§ç¢ºå®š
+ e.preventDefault();
+
+ var oe = Neo.painter;
+ var text = oe.inputText;
+ if (text) {
+ oe._pushUndo();
+ this.drawText(oe);
+ oe.updateDestCanvas(0, 0, oe.canvasWidth, oe.canvasHeight, true);
+
+ text.style.display = "none";
+ text.blur();
+ }
+ }
+};
+
+Neo.TextTool.prototype.kill = function(oe) {
+ Neo.painter.hideInputText();
+};
+
+Neo.TextTool.prototype.drawText = function(oe) {
+ var text = oe.inputText;
+
+ // unescape entities
+ //var tmp = document.createElement("textarea");
+ //tmp.innerHTML = text.innerHTML;
+ //var string = tmp.value;
+
+ var string = text.textContent || text.innerText;
+
+ if (string.length <= 0) return;
+ oe.doText(this.startX, this.startY, string, text.style.fontSize);
+};
+
+Neo.TextTool.prototype.loadStates = function() {
+ var reserve = this.getReserve();
+ if (reserve) {
+ Neo.painter.lineWidth = reserve.size;
+ Neo.painter.alpha = 1.0;
+ Neo.updateUI();
+ };
+};
+
+/*
+ -------------------------------------------------------------------------
+ Dummy(何もã—ãªã„時)
+ -------------------------------------------------------------------------
+*/
+
+Neo.DummyTool = function() {};
+Neo.DummyTool.prototype = new Neo.ToolBase();
+Neo.DummyTool.prototype.type = Neo.Painter.TOOLTYPE_NONE;
+Neo.DummyTool.prototype.isUpMove = false;
+
+Neo.DummyTool.prototype.downHandler = function(oe) {
+};
+
+Neo.DummyTool.prototype.upHandler = function(oe) {
+ oe.popTool();
+};
+
+Neo.DummyTool.prototype.moveHandler = function(oe) {};
+Neo.DummyTool.prototype.upMoveHandler = function(oe) {}
+Neo.DummyTool.prototype.rollOverHandler= function(oe) {}
+Neo.DummyTool.prototype.rollOutHandler= function(oe) {}
+
+'use strict';
+
+Neo.CommandBase = function() {
+};
+Neo.CommandBase.prototype.data;
+Neo.CommandBase.prototype.execute = function() {}
+
+
+/*
+ ---------------------------------------------------
+ ZOOM
+ ---------------------------------------------------
+*/
+Neo.ZoomPlusCommand = function(data) {this.data = data};
+Neo.ZoomPlusCommand.prototype = new Neo.CommandBase();
+Neo.ZoomPlusCommand.prototype.execute = function() {
+ if (this.data.zoom < 12) {
+ this.data.setZoom(this.data.zoom + 1);
+ }
+ Neo.resizeCanvas();
+ Neo.painter.updateDestCanvas();
+};
+
+Neo.ZoomMinusCommand = function(data) {this.data = data};
+Neo.ZoomMinusCommand.prototype = new Neo.CommandBase();
+Neo.ZoomMinusCommand.prototype.execute = function() {
+ if (this.data.zoom >= 2) {
+ this.data.setZoom(this.data.zoom - 1);
+ }
+ Neo.resizeCanvas();
+ Neo.painter.updateDestCanvas();
+};
+
+/*
+ ---------------------------------------------------
+ UNDO
+ ---------------------------------------------------
+*/
+Neo.UndoCommand = function(data) {this.data = data};
+Neo.UndoCommand.prototype = new Neo.CommandBase();
+Neo.UndoCommand.prototype.execute = function() {
+ this.data.undo();
+};
+
+Neo.RedoCommand = function(data) {this.data = data};
+Neo.RedoCommand.prototype = new Neo.CommandBase();
+Neo.RedoCommand.prototype.execute = function() {
+ this.data.redo();
+};
+
+
+Neo.WindowCommand = function(data) {this.data = data};
+Neo.WindowCommand.prototype = new Neo.CommandBase();
+Neo.WindowCommand.prototype.execute = function() {
+ if (Neo.fullScreen) {
+ if (confirm(Neo.translate("ページビュー?"))) {
+ Neo.fullScreen = false;
+ Neo.updateWindow();
+ }
+ } else {
+ if (confirm(Neo.translate("ウィンドウビュー?"))) {
+ Neo.fullScreen = true;
+ Neo.updateWindow();
+ }
+ }
+};
+
+Neo.SubmitCommand = function(data) {this.data = data};
+Neo.SubmitCommand.prototype = new Neo.CommandBase();
+Neo.SubmitCommand.prototype.execute = function() {
+ var board = location.href.replace(/[^/]*$/, '');
+ console.log("submit: " + board);
+ this.data.submit(board);
+};
+
+Neo.CopyrightCommand = function(data) {this.data = data};
+Neo.CopyrightCommand.prototype = new Neo.CommandBase();
+Neo.CopyrightCommand.prototype.execute = function() {
+ var url = "http://github.com/funige/neo/";
+ if (confirm(Neo.translate("PaintBBS NEOã¯ã€ãŠçµµæãã—ãƒæŽ²ç¤ºæ¿ PaintBBS (©2000-2004 ã—ãƒã¡ã‚ƒã‚“) ã‚’html5化ã™ã‚‹ãƒ—ロジェクトã§ã™ã€‚\n\nPaintBBS NEOã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ã‚’表示ã—ã¾ã™ã‹ï¼Ÿ") + "\n")) {
+ Neo.openURL(url);
+ }
+};
+
+'use strict';
+
+Neo.getModifier = function(e) {
+ if (e.shiftKey) {
+ return 'shift';
+
+ } else if (e.button == 2 || e.ctrlKey || e.altKey || Neo.painter.virtualRight) {
+ return 'right';
+ }
+ return null;
+}
+
+/*
+ -------------------------------------------------------------------------
+ Button
+ -------------------------------------------------------------------------
+*/
+
+Neo.Button = function() {};
+Neo.Button.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.selected = false;
+ this.isMouseDown = false;
+
+ var ref = this;
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ this.element.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ e.preventDefault();
+ }, true);
+ this.element.addEventListener("touchend", function(e) {
+ ref._mouseUpHandler(e);
+ }, true);
+
+
+ this.element.className = (!this.params.type == "fill") ? "button" : "buttonOff";
+
+ return this;
+};
+
+Neo.Button.prototype._mouseDownHandler = function(e) {
+ if (Neo.painter.isUIPaused()) return;
+ this.isMouseDown = true;
+
+ if ((this.params.type == "fill") && (this.selected == false)) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.setSelected((this.selected) ? false : true);
+ }
+ Neo.painter.setToolByType(Neo.Painter.TOOLTYPE_FILL);
+ }
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+Neo.Button.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+Neo.Button.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.Button.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.Button.prototype.setSelected = function(selected) {
+ if (selected) {
+ this.element.className = "buttonOn";
+ } else {
+ this.element.className = "buttonOff";
+ }
+ this.selected = selected;
+};
+
+Neo.Button.prototype.update = function() {
+};
+
+/*
+ -------------------------------------------------------------------------
+ Right Button
+ -------------------------------------------------------------------------
+*/
+
+Neo.RightButton;
+
+Neo.RightButton = function() {};
+Neo.RightButton.prototype = new Neo.Button();
+
+Neo.RightButton.prototype.init = function(name, params) {
+ Neo.Button.prototype.init.call(this, name, params);
+ this.params.type = "right";
+ return this;
+}
+
+Neo.RightButton.prototype._mouseDownHandler = function(e) {
+};
+
+Neo.RightButton.prototype._mouseUpHandler = function(e) {
+ this.setSelected(!this.selected)
+};
+
+Neo.RightButton.prototype._mouseOutHandler = function(e) {
+};
+
+Neo.RightButton.prototype.setSelected = function (selected) {
+ if (selected) {
+ this.element.className = "buttonOn";
+ Neo.painter.virtualRight = true;
+ } else {
+ this.element.className = "buttonOff";
+ Neo.painter.virtualRight = false;
+ }
+ this.selected = selected;
+};
+
+Neo.RightButton.clear = function () {
+ var right = Neo.rightButton;
+ right.setSelected(false);
+};
+
+/*
+ -------------------------------------------------------------------------
+ Fill Button
+ -------------------------------------------------------------------------
+*/
+
+Neo.FillButton;
+
+Neo.FillButton = function() {};
+Neo.FillButton.prototype = new Neo.Button();
+
+Neo.FillButton.prototype.init = function(name, params) {
+ Neo.Button.prototype.init.call(this, name, params);
+ this.params.type = "fill";
+ return this;
+}
+
+/*
+ -------------------------------------------------------------------------
+ ColorTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.colorTips = [];
+
+Neo.ColorTip = function() {};
+Neo.ColorTip.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ this.selected = (this.name == "color1") ? true : false;
+ this.isMouseDown = false;
+
+ var ref = this;
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ this.element.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ e.preventDefault();
+ }, true);
+ this.element.addEventListener("touchend", function(e) {
+ ref._mouseUpHandler(e);
+ }, true);
+
+ this.element.className = "colorTipOff";
+
+ var index = parseInt(this.name.slice(5)) - 1;
+ this.element.style.left = (index % 2) ? "0px" : "26px";
+ this.element.style.top = Math.floor(index / 2) * 21 + "px";
+
+ // base64 ColorTip.png
+ this.element.innerHTML = "<img style='max-width:44px;' src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAASCAYAAAAg9DzcAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAANklEQVRIx+3OAQkAMADDsO3+Pe8qCj+0Akq6bQFqS2wTCpwE+R4IiyVYsGDBggULfirBgn8HX7BzCRwDx1QeAAAAAElFTkSuQmCC' />"
+
+ this.setColor(Neo.config.colors[params.index - 1]);
+
+ this.setSelected(this.selected);
+ Neo.colorTips.push(this);
+};
+
+Neo.ColorTip.prototype._mouseDownHandler = function(e) {
+ if (Neo.painter.isUIPaused()) return;
+ this.isMouseDown = true;
+
+ for (var i = 0; i < Neo.colorTips.length; i++) {
+ var colorTip = Neo.colorTips[i];
+ if (this == colorTip) {
+ switch (Neo.getModifier(e)) {
+ case 'shift':
+ this.setColor(Neo.config.colors[this.params.index - 1]);
+ break;
+ case 'right':
+ this.setColor(Neo.painter.foregroundColor);
+ break;
+ }
+
+// if (e.shiftKey) {
+// this.setColor(Neo.config.colors[this.params.index - 1]);
+// } else if (e.button == 2 || e.ctrlKey || e.altKey ||
+// Neo.painter.virtualRight) {
+// this.setColor(Neo.painter.foregroundColor);
+// }
+ }
+ colorTip.setSelected(this == colorTip) ? true : false;
+ }
+ Neo.painter.setColor(this.color);
+ Neo.updateUIColor(true, false);
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+Neo.ColorTip.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+Neo.ColorTip.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.ColorTip.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.ColorTip.prototype.setSelected = function(selected) {
+ if (selected) {
+ this.element.className = "colorTipOn";
+ } else {
+ this.element.className = "colorTipOff";
+ }
+ this.selected = selected;
+};
+
+Neo.ColorTip.prototype.setColor = function(color) {
+ this.color = color;
+ this.element.style.backgroundColor = color;
+};
+
+Neo.ColorTip.getCurrent = function() {
+ for (var i = 0; i < Neo.colorTips.length; i++) {
+ var colorTip = Neo.colorTips[i];
+ if (colorTip.selected) return colorTip;
+ }
+ return null;
+};
+
+/*
+ -------------------------------------------------------------------------
+ ToolTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.toolTips = [];
+Neo.toolButtons = [];
+
+Neo.ToolTip = function() {};
+
+Neo.ToolTip.prototype.prevMode = -1;
+
+Neo.ToolTip.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.params.type = this.element.id;
+ this.name = name;
+ this.mode = 0;
+
+ this.isMouseDown = false;
+
+ var ref = this;
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.onmouseup = function(e) { ref._mouseUpHandler(e); }
+ this.element.onmouseover = function(e) { ref._mouseOverHandler(e); }
+ this.element.onmouseout = function(e) { ref._mouseOutHandler(e); }
+ this.element.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ e.preventDefault();
+ }, true);
+ this.element.addEventListener("touchend", function(e) {
+ ref._mouseUpHandler(e);
+ }, true);
+
+ this.selected = (this.params.type == "pen") ? true : false;
+ this.setSelected(this.selected);
+
+ this.element.innerHTML = "<canvas width=46 height=18></canvas><div class='label'></div>";
+ this.canvas = this.element.getElementsByTagName('canvas')[0];
+ this.label = this.element.getElementsByTagName('div')[0];
+
+ this.update();
+ return this;
+};
+
+Neo.ToolTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ if (this.isTool) {
+ if (this.selected == false) {
+ for (var i = 0; i < Neo.toolButtons.length; i++) {
+ var toolTip = Neo.toolButtons[i];
+ toolTip.setSelected((this == toolTip) ? true : false);
+ }
+
+ } else {
+ var length = this.toolStrings.length;
+ if (Neo.getModifier(e) == "right") {
+ this.mode--;
+ if (this.mode < 0) this.mode = length - 1;
+
+ } else {
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ }
+ }
+ Neo.painter.setToolByType(this.tools[this.mode]);
+ this.update();
+ }
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+
+Neo.ToolTip.prototype._mouseUpHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseup) this.onmouseup(this);
+ }
+};
+
+Neo.ToolTip.prototype._mouseOutHandler = function(e) {
+ if (this.isMouseDown) {
+ this.isMouseDown = false;
+ if (this.onmouseout) this.onmouseout(this);
+ }
+};
+Neo.ToolTip.prototype._mouseOverHandler = function(e) {
+ if (this.onmouseover) this.onmouseover(this);
+};
+
+Neo.ToolTip.prototype.setSelected = function(selected) {
+ if (this.fixed) {
+ this.element.className = "toolTipFixed";
+
+ } else {
+ if (selected) {
+ this.element.className = "toolTipOn";
+ } else {
+ this.element.className = "toolTipOff";
+ }
+ }
+ this.selected = selected;
+};
+
+Neo.ToolTip.prototype.update = function() {};
+
+Neo.ToolTip.prototype.draw = function(c) {
+ if (this.hasTintImage) {
+ if (typeof c != "string") c = Neo.painter.getColorString(c);
+ var ctx = this.canvas.getContext("2d");
+
+ if (this.prevMode != this.mode) {
+ this.prevMode = this.mode;
+
+ var img = new Image();
+ img.src = this.toolIcons[this.mode];
+ img.onload = function() {
+ var ref = this;
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.drawTintImage(ctx, img, c, 0, 0);
+ }.bind(this);
+
+ } else {
+ this.tintImage(ctx, c);
+ }
+ }
+};
+
+Neo.ToolTip.prototype.drawTintImage = function(ctx, img, c, x, y) {
+ ctx.drawImage(img, x, y);
+ this.tintImage(ctx, c);
+};
+
+Neo.ToolTip.prototype.tintImage = function(ctx, c) {
+ c = (Neo.painter.getColor(c) & 0xffffff);
+
+ var imageData = ctx.getImageData(0, 0, 46, 18);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+
+ for (var i = 0; i < buf32.length; i++) {
+ var a = buf32[i] & 0xff000000;
+ if (a) {
+ buf32[i] = buf32[i] & a | c;
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+};
+
+Neo.ToolTip.bezier = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAT0lEQVRIx+3SQQoAIAhE0en+h7ZVEEKBZrX5b5sjKknAkRYpNslaMLPq44ZI9wwHs0vMQ/v87u0Kk8xfsaI242jbMdjPi5Y0r/zTAAAAD3UOjRf9jcO4sgAAAABJRU5ErkJggg==";
+Neo.ToolTip.blur = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAASUlEQVRIx+3VMQ4AIAgEQeD/f8bWWBnJYUh2SgtgK82G8/MhzVKwxOtTLgIUx6tDout4laiPIICA0Qj4bXxAy0+8LZP9yACAJwsqkggS55eiZgAAAABJRU5ErkJggg==";
+Neo.ToolTip.blurrect = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAX0lEQVRIx+2XQQ4AEAwEt+I7/v+8Org6lJKt6NzLjjYE8DAKtLpYoDeCCCC7tYUd3ru2qQOzDTyndhJzB6KSAmxSgM0fAlGuzBnmlziqxB8jFJkUYJMCbAQYPxt2kF06fvYKgjPBO/IAAAAASUVORK5CYII=";
+Neo.ToolTip.brush = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAQUlEQVRIx2NgGOKAEcb4z8CweRA4xpdUPSxofJ8BdP8WcjQxDaCDqQLQY4CsUBgFo2AUjIJRMApGwSgYBaNgZAIA0CoDwDbZu8oAAAAASUVORK5CYII=";
+Neo.ToolTip.burn = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAPklEQVRIx+3PMRIAMAQAQbzM0/0sKZPeiDG57TQ4keH0Htx9VR+MCM1vOezl8xUsv4IAAkYjoBsB3QgAgL9tYXgF19rh9yoAAAAASUVORK5CYII=";
+Neo.ToolTip.copy = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAW0lEQVRIx+2XMQoAIAwDU/E7/v95Orh2KMUSC7m5Qs6AUqAxG1gzOLirwxhgmXOjOlg1oQY8sjf2mvYNSICNBNhIgE3oH/jlzfdo34AE2EiATXsBA+5mww6S5QASDwSGMt8ouwAAAABJRU5ErkJggg==";
+Neo.ToolTip.copy2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAN0lEQVRIx+3PwQkAIBADwdPKt3MtQVCOPNz5B7JV0pNxOwRW9zng+G92n+hmQJoBaQakGSBJf9tyBgQUV/fKCAAAAABJRU5ErkJggg==";
+Neo.ToolTip.ellipse = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAATklEQVRIx+2VMQ4AIAgD6/8fjbOJi1LFmt4OPQ0KIE7LNgggCBLbHkuFM9lM+Om+QwDjpksyb4tT86vlvzgEbYxefQPyv5D8HjDGGGOk6b3jJ+lYubd8AAAAAElFTkSuQmCC";
+Neo.ToolTip.ellipsefill = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAVUlEQVRIx+2VURIAEAgFc/9D5waSHpV5+43ZHRMizRnRA1REARLHHq6NCFl01Nail+LeEDMgU34nYhlQQd6K+PsGKkSEZyArBPoK3Y6K/AOEEEJIayZHbhIKjkZrFwAAAABJRU5ErkJggg==";
+Neo.ToolTip.eraser = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAABQElEQVRIx+1WQY7CMAwcI37Cad+yXOgH4Gu8gAt9CtrDirfMHjZJbbcktVSpQnROSeMkY3vsFHhzSG3xfLpz/JVmG0mIqDkIMcc6+7Kejx6fdb0dq7w09rVFkrjejrMOunQ9vg7f/5QEIAd6E1Eo38WF8fF7n8sdALCrLerIzoFI4sI0Vtv1SYZ8CVbeF7tzF7JugIkVkxOauc6CIe8842S+XmMfsq7TN9LRTngZmTmVD4SrnzYaGYhFoxCWgajXuMjYGTuJ3dlwIBIN3U0cUVqLXCs5E7YeVsvAYJul5HWeLUhL3EpstQwooqoOTEHDOebpMn7ngkUsg3RotU8X1MkuVDrYohkIupC0YArX6T+PfX3kcbQLNV/iCKi6EB3xqXdAZ0JKthZ8B0QEl673NIEX/0I/z36Rf6ENGzZ8EP4A8Lp+9e9VWC4AAAAASUVORK5CYII=";
+Neo.ToolTip.flip = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAZklEQVRIx+2XQQoAIAgE1+g7/f95degWHSyTTXDOhTsSiUBgOtCq8mD3DiOA3NxTCVgKaLA0qHiFOsHSnC8ELKQAmxRgE15APQfWv9pzLjwX+CXsjvBPKAXYpACb8AICzM2GHeSWAfVOCIiJuQ9tAAAAAElFTkSuQmCC";
+Neo.ToolTip.freehand = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAdUlEQVRIx+2WUQrAMAhD3dj9r+y+VoSyLhYDynzQv1qiJlCR4hzeAhVRsiC3Jkj0c5hN7Lx7IQ9SphLE1ICdwko420purEWQuywN3pqxgcw2+WwAtU1GzoqiLZNwZBvMAIcO8y3YKUO8mkbmjPzjK9E0TUPjBoeyLAS0usjLAAAAAElFTkSuQmCC";
+Neo.ToolTip.line = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAU0lEQVRIx+2UQQ4AIAjD8P+PxivRGDQC47C+oN1hIgTLQAt4qIga2c23XYAVPkm3CVhlb4ShAa/rQgMi1i0NyFg3LaBq3bAA1LpfAd7/EkIIIR2YXFYSCpWS8w8AAAAASUVORK5CYII=";
+Neo.ToolTip.merge = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAW0lEQVRIx+2XQQrAQAgDx9Lv9JF9+e6h54IINlgyZ4UMOYgwmAXXmRxc3WECorJ3dAfrJtXAC7c6PPygAQuosYAaC6hJ3YHqlfyC8Q1YQI0F1IwXCHg+G3WQKhvwgwUFmFyYbwAAAABJRU5ErkJggg==";
+Neo.ToolTip.pen = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAK0lEQVRIx+3OsQkAMAwDQXn/oe3WfSAEctd9I5TA32pHJ/3AoTpfAQCAGwaa5AICJLKWSQAAAABJRU5ErkJggg==";
+Neo.ToolTip.rect = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAQElEQVRIx+3TMQ4AIAhD0WK8/5VxdcIYY8rw3wok7YAEr6iGKaU74BY0ro+6FKhyDHe4VxRwm6eFLn8AAADwwQIwTQgGo9ZMywAAAABJRU5ErkJggg==";
+Neo.ToolTip.rectfill = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAANElEQVRIx+3PIQ4AIBADwcL//3xYBMEgLiQztmab0GvcxkqqO3ALPbbO7rBXDnRzAADgYwvqDwIMJlGb5QAAAABJRU5ErkJggg==";
+Neo.ToolTip.text = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAcUlEQVRIx+2VwQ7AIAhDy7L//2V2WmIYg+ky2KEv8aCCqYQqQMgrJNpUQMXEKKDmAPHyspgSrBBvLZu3cQqZEdwhfusq0KdkVR5HlFfBvpI0mtIzeusFot7vFPqYuzZYMXUFlzc+qrIn7tf/ACGEkIwDlEQ94YZjzcgAAAAASUVORK5CYII=";
+Neo.ToolTip.tone = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAATCAYAAADWOo4fAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAO0lEQVRIx+3PIQ4AMAgEwaP//zNVVZUELiQ7CgWstFy8IaVsPhT1Lb/T+fQEAtwIcCPAjQC39QEAgJIL6DQCFhAqsRkAAAAASUVORK5CYII=";
+
+/*
+ -------------------------------------------------------------------------
+ PenTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.penTip;
+
+Neo.PenTip = function() {};
+Neo.PenTip.prototype = new Neo.ToolTip();
+
+Neo.PenTip.prototype.tools = [Neo.Painter.TOOLTYPE_PEN,
+ Neo.Painter.TOOLTYPE_BRUSH,
+ Neo.Painter.TOOLTYPE_TEXT];
+
+Neo.PenTip.prototype.hasTintImage = true;
+Neo.PenTip.prototype.toolIcons = [Neo.ToolTip.pen,
+ Neo.ToolTip.brush,
+ Neo.ToolTip.text];
+
+Neo.PenTip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("鉛筆"),
+ Neo.translate("水彩"),
+ Neo.translate("テキスト")];
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.PenTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ if (this.label) {
+ this.label.innerHTML = this.toolStrings[this.mode];
+ }
+};
+
+/*
+ -------------------------------------------------------------------------
+ Pen2Tip
+ -------------------------------------------------------------------------
+*/
+
+Neo.pen2Tip;
+
+Neo.Pen2Tip = function() {};
+Neo.Pen2Tip.prototype = new Neo.ToolTip();
+
+Neo.Pen2Tip.prototype.tools = [Neo.Painter.TOOLTYPE_TONE,
+ Neo.Painter.TOOLTYPE_BLUR,
+ Neo.Painter.TOOLTYPE_DODGE,
+ Neo.Painter.TOOLTYPE_BURN];
+
+Neo.Pen2Tip.prototype.hasTintImage = true;
+Neo.Pen2Tip.prototype.toolIcons = [Neo.ToolTip.tone,
+ Neo.ToolTip.blur,
+ Neo.ToolTip.burn,
+ Neo.ToolTip.burn];
+
+Neo.Pen2Tip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("トーン"),
+ Neo.translate("ã¼ã‹ã—"),
+ Neo.translate("覆ã„焼ã"),
+ Neo.translate("焼ãè¾¼ã¿")];
+
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.Pen2Tip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ switch (this.tools[this.mode]) {
+ case Neo.Painter.TOOLTYPE_TONE:
+ this.drawTone(Neo.painter.foregroundColor);
+ break;
+
+ case Neo.Painter.TOOLTYPE_DODGE:
+ this.draw(0xffc0c0c0);
+ break;
+
+ case Neo.Painter.TOOLTYPE_BURN:
+ this.draw(0xff404040);
+ break;
+
+ default:
+ this.draw(Neo.painter.foregroundColor);
+ break;
+ }
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.Pen2Tip.prototype.drawTone = function() {
+ var ctx = this.canvas.getContext("2d");
+
+ var imageData = ctx.getImageData(0, 0, 46, 18);
+ var buf32 = new Uint32Array(imageData.data.buffer);
+ var buf8 = new Uint8ClampedArray(imageData.data.buffer);
+ var c = Neo.painter.getColor() | 0xff000000;
+ var a = Math.floor(Neo.painter.alpha * 255);
+ var toneData = Neo.painter.getToneData(a);
+
+ for (var j = 0; j < 18; j++) {
+ for (var i = 0; i < 46; i++) {
+ if (j >= 1 && j < 12 &&
+ i >= 2 && i < 26 &&
+ toneData[(i%4) + (j%4) * 4]) {
+ buf32[j * 46 + i] = c;
+
+ } else {
+ buf32[j * 46 + i] = 0;
+ }
+ }
+ }
+ imageData.data.set(buf8);
+ ctx.putImageData(imageData, 0, 0);
+
+ this.prevMode = this.mode;
+};
+
+
+/*
+ -------------------------------------------------------------------------
+ EraserTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.eraserTip;
+
+Neo.EraserTip = function() {};
+Neo.EraserTip.prototype = new Neo.ToolTip();
+
+Neo.EraserTip.prototype.tools = [Neo.Painter.TOOLTYPE_ERASER,
+ Neo.Painter.TOOLTYPE_ERASERECT,
+ Neo.Painter.TOOLTYPE_ERASEALL];
+
+Neo.EraserTip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("消ã—ペン"),
+ Neo.translate("消ã—四角"),
+ Neo.translate("全消ã—")];
+
+ this.drawOnce = false;
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.EraserTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ if (this.drawOnce == false) {
+ this.draw();
+ this.drawOnce = true;
+ }
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.EraserTip.prototype.draw = function() {
+ var ctx = this.canvas.getContext("2d");
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ var img = new Image();
+
+ img.src = Neo.ToolTip.eraser;
+ img.onload = function() {
+ ctx.drawImage(img, 0, 0);
+ };
+};
+
+/*
+ -------------------------------------------------------------------------
+ EffectTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.effectTip;
+
+Neo.EffectTip = function() {};
+Neo.EffectTip.prototype = new Neo.ToolTip();
+
+Neo.EffectTip.prototype.tools = [Neo.Painter.TOOLTYPE_RECTFILL,
+ Neo.Painter.TOOLTYPE_RECT,
+ Neo.Painter.TOOLTYPE_ELLIPSEFILL,
+ Neo.Painter.TOOLTYPE_ELLIPSE];
+
+Neo.EffectTip.prototype.hasTintImage = true;
+Neo.EffectTip.prototype.toolIcons = [Neo.ToolTip.rectfill,
+ Neo.ToolTip.rect,
+ Neo.ToolTip.ellipsefill,
+ Neo.ToolTip.ellipse];
+
+Neo.EffectTip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("四角"),
+ Neo.translate("線四角"),
+ Neo.translate("楕円"),
+ Neo.translate("線楕円")];
+
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.EffectTip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+ -------------------------------------------------------------------------
+ Effect2Tip
+ -------------------------------------------------------------------------
+*/
+
+Neo.effect2Tip;
+
+Neo.Effect2Tip = function() {};
+Neo.Effect2Tip.prototype = new Neo.ToolTip();
+
+Neo.Effect2Tip.prototype.tools = [Neo.Painter.TOOLTYPE_COPY,
+ Neo.Painter.TOOLTYPE_MERGE,
+ Neo.Painter.TOOLTYPE_BLURRECT,
+ Neo.Painter.TOOLTYPE_FLIP_H,
+ Neo.Painter.TOOLTYPE_FLIP_V,
+ Neo.Painter.TOOLTYPE_TURN];
+
+Neo.Effect2Tip.prototype.hasTintImage = true;
+Neo.Effect2Tip.prototype.toolIcons = [Neo.ToolTip.copy,
+ Neo.ToolTip.merge,
+ Neo.ToolTip.blurrect,
+ Neo.ToolTip.flip,
+ Neo.ToolTip.flip,
+ Neo.ToolTip.flip];
+
+Neo.Effect2Tip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("コピー"),
+ Neo.translate("レイヤçµåˆ"),
+ Neo.translate("角å–ã‚Š"),
+ Neo.translate("å·¦å³å転"),
+ Neo.translate("上下å転"),
+ Neo.translate("傾ã‘")];
+
+ this.isTool = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+
+ this.img = document.createElement("img");
+ this.img.src = Neo.ToolTip.copy2;
+ this.element.appendChild(this.img);
+ return this;
+};
+
+Neo.Effect2Tip.prototype.update = function() {
+ for (var i = 0; i < this.tools.length; i++) {
+ if (Neo.painter.tool.type == this.tools[i]) this.mode = i;
+ }
+
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+ -------------------------------------------------------------------------
+ MaskTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.maskTip;
+
+Neo.MaskTip = function() {};
+Neo.MaskTip.prototype = new Neo.ToolTip();
+
+Neo.MaskTip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("通常"),
+ Neo.translate("マスク"),
+ Neo.translate("逆ï¾ï½½ï½¸"),
+ Neo.translate("加算"),
+ Neo.translate("逆加算")];
+
+ this.fixed = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.MaskTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ if (Neo.getModifier(e) == "right") {
+ Neo.painter.maskColor = Neo.painter.foregroundColor;
+
+ } else {
+ var length = this.toolStrings.length;
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ Neo.painter.maskType = this.mode;
+ }
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+}
+
+Neo.MaskTip.prototype.update = function() {
+ this.draw(Neo.painter.maskColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+Neo.MaskTip.prototype.draw = function(c) {
+ if (typeof c != "string") c = Neo.painter.getColorString(c);
+
+ var ctx = this.canvas.getContext("2d");
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ ctx.fillStyle = c;
+ ctx.fillRect(1, 1, 43, 9);
+};
+
+/*
+ -------------------------------------------------------------------------
+ DrawTip
+ -------------------------------------------------------------------------
+*/
+
+Neo.drawTip;
+
+Neo.DrawTip = function() {};
+Neo.DrawTip.prototype = new Neo.ToolTip();
+
+Neo.DrawTip.prototype.hasTintImage = true;
+Neo.DrawTip.prototype.toolIcons = [Neo.ToolTip.freehand,
+ Neo.ToolTip.line,
+ Neo.ToolTip.bezier];
+
+Neo.DrawTip.prototype.init = function(name, params) {
+ this.toolStrings = [Neo.translate("手書ã"),
+ Neo.translate("ç›´ç·š"),
+ Neo.translate("BZ曲線")];
+
+ this.fixed = true;
+ Neo.ToolTip.prototype.init.call(this, name, params);
+ return this;
+};
+
+Neo.DrawTip.prototype._mouseDownHandler = function(e) {
+ this.isMouseDown = true;
+
+ var length = this.toolStrings.length;
+
+ if (Neo.getModifier(e) == "right") {
+ this.mode--;
+ if (this.mode < 0) this.mode = length - 1;
+
+ } else {
+ this.mode++;
+ if (this.mode >= length) this.mode = 0;
+ }
+ Neo.painter.drawType = this.mode;
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+}
+
+Neo.DrawTip.prototype.update = function() {
+ this.mode = Neo.painter.drawType;
+ this.draw(Neo.painter.foregroundColor);
+ this.label.innerHTML = this.toolStrings[this.mode];
+};
+
+/*
+ -------------------------------------------------------------------------
+ ColorSlider
+ -------------------------------------------------------------------------
+*/
+
+Neo.sliders = [];
+
+Neo.ColorSlider = function() {};
+
+Neo.ColorSlider.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+ this.value = 0;
+ this.type = this.params.type;
+
+ this.element.className = "colorSlider";
+ this.element.innerHTML = "<div class='slider'></div><div class='label'></div>";
+ this.element.innerHTML += "<div class='hit'></div>";
+
+ this.slider = this.element.getElementsByClassName('slider')[0];
+ this.label = this.element.getElementsByClassName('label')[0];
+ this.hit = this.element.getElementsByClassName('hit')[0];
+ this.hit['data-slider'] = params.type;
+
+ switch (this.type) {
+ case Neo.SLIDERTYPE_RED:
+ this.prefix = "R";
+ this.slider.style.backgroundColor = "#fa9696";
+ break;
+ case Neo.SLIDERTYPE_GREEN:
+ this.prefix = "G";
+ this.slider.style.backgroundColor = "#82f238";
+ break;
+ case Neo.SLIDERTYPE_BLUE:
+ this.prefix = "B";
+ this.slider.style.backgroundColor = "#8080ff";
+ break;
+ case Neo.SLIDERTYPE_ALPHA:
+ this.prefix = "A";
+ this.slider.style.backgroundColor = "#aaaaaa";
+ this.value = 255;
+ break;
+ }
+
+ this.update();
+ return this;
+};
+
+Neo.ColorSlider.prototype.downHandler = function(x, y) {
+ if (Neo.painter.isShiftDown) {
+ this.shift(x, y);
+
+ } else {
+ this.slide(x, y);
+ }
+};
+
+Neo.ColorSlider.prototype.moveHandler = function(x, y) {
+ this.slide(x, y);
+ //event.preventDefault();
+};
+
+Neo.ColorSlider.prototype.upHandler = function(x, y) {
+};
+
+Neo.ColorSlider.prototype.shift = function(x, y) {
+ var value;
+ if (x >= 0 && x < 60 && y >= 0 && y <= 15) {
+ var v = Math.floor((x - 5) * 5.0);
+ var min = (this.type == Neo.SLIDERTYPE_ALPHA) ? 1 : 0;
+
+ value = Math.max(Math.min(v, 255), min);
+ if (this.value > value || this.value == 255) {
+ this.value--;
+ } else {
+ this.value++;
+ }
+ this.value = Math.max(Math.min(this.value, 255), min);
+ this.value0 = this.value;
+ this.x0 = x;
+ }
+
+ if (this.type == Neo.SLIDERTYPE_ALPHA) {
+ Neo.painter.alpha = this.value / 255.0;
+ this.update();
+ Neo.updateUIColor(false, false);
+
+ } else {
+ var r = Neo.sliders[Neo.SLIDERTYPE_RED].value;
+ var g = Neo.sliders[Neo.SLIDERTYPE_GREEN].value;
+ var b = Neo.sliders[Neo.SLIDERTYPE_BLUE].value;
+
+ Neo.painter.setColor(r<<16 | g<<8 | b);
+ Neo.updateUIColor(true, true);
+ }
+};
+
+Neo.ColorSlider.prototype.slide = function(x, y) {
+ var value;
+ if (x >= 0 && x < 60 && y >= 0 && y <= 15) {
+ var v = Math.floor((x - 5) * 5.0);
+ value = Math.round(v / 5) * 5;
+
+ this.value0 = value;
+ this.x0 = x;
+
+ } else {
+ var d = (x - this.x0) / 3.0;
+ value = this.value0 + d;
+ }
+
+ var min = (this.type == Neo.SLIDERTYPE_ALPHA) ? 1 : 0;
+ this.value = Math.max(Math.min(value, 255), min);
+
+ if (this.type == Neo.SLIDERTYPE_ALPHA) {
+ Neo.painter.alpha = this.value / 255.0;
+ this.update();
+ Neo.updateUIColor(false, false);
+
+ } else {
+ var r = Neo.sliders[Neo.SLIDERTYPE_RED].value;
+ var g = Neo.sliders[Neo.SLIDERTYPE_GREEN].value;
+ var b = Neo.sliders[Neo.SLIDERTYPE_BLUE].value;
+ var color = (r<<16 | g<<8 | b);
+
+ var colorTip = Neo.ColorTip.getCurrent()
+ if (colorTip) {
+ colorTip.setColor(Neo.painter.getColorString(color))
+ }
+
+ Neo.painter.setColor(color);
+ // Neo.updateUIColor(true, true);
+ }
+};
+
+Neo.ColorSlider.prototype.update = function() {
+ var color = Neo.painter.getColor();
+ var alpha = Neo.painter.alpha * 255;
+
+ switch (this.type) {
+ case Neo.SLIDERTYPE_RED: this.value = (color & 0x0000ff); break;
+ case Neo.SLIDERTYPE_GREEN: this.value = (color & 0x00ff00) >> 8; break;
+ case Neo.SLIDERTYPE_BLUE: this.value = (color & 0xff0000) >> 16; break;
+ case Neo.SLIDERTYPE_ALPHA: this.value = alpha; break;
+ }
+
+ var width = this.value * 49.0 / 255.0;
+ width = Math.max(Math.min(48, width), 1);
+
+ this.slider.style.width = width.toFixed(2) + "px";
+ this.label.innerHTML = this.prefix + this.value.toFixed(0);
+};
+
+/*
+ -------------------------------------------------------------------------
+ SizeSlider
+ -------------------------------------------------------------------------
+*/
+
+Neo.SizeSlider = function() {};
+
+Neo.SizeSlider.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+ this.value = this.value0 = 1;
+
+ this.element.className = "sizeSlider";
+ this.element.innerHTML = "<div class='slider'></div><div class='label'></div>";
+ this.element.innerHTML += "<div class='hit'></div>"
+
+ this.slider = this.element.getElementsByClassName('slider')[0];
+ this.label = this.element.getElementsByClassName('label')[0];
+ this.hit = this.element.getElementsByClassName('hit')[0];
+ this.hit['data-slider'] = params.type;
+
+ this.slider.style.backgroundColor = Neo.painter.foregroundColor;
+ this.update();
+ return this;
+};
+
+Neo.SizeSlider.prototype.downHandler = function(x, y) {
+ if (Neo.painter.isShiftDown) {
+ this.shift(x, y);
+
+ } else {
+ this.value0 = this.value;
+ this.y0 = y;
+ this.slide(x, y);
+ }
+};
+
+Neo.SizeSlider.prototype.moveHandler = function(x, y) {
+ this.slide(x, y);
+ //event.preventDefault();
+};
+
+Neo.SizeSlider.prototype.upHandler = function(x, y) {
+};
+
+Neo.SizeSlider.prototype.shift = function(x, y) {
+ var value0 = Neo.painter.lineWidth;
+ var value;
+
+ if (!Neo.painter.tool.alt) {
+ var v = Math.floor((y - 4) * 30.0 / 33.0);
+
+ value = Math.max(Math.min(v, 30), 1);
+ if (value0 > value || value0 == 30) {
+ value0--;
+ } else {
+ value0++;
+ }
+ this.setSize(value0);
+ }
+};
+
+Neo.SizeSlider.prototype.slide = function(x, y) {
+ var value;
+ if (!Neo.painter.tool.alt) {
+ if (x >= 0 && x < 48 && y >= 0 && y < 41) {
+ var v = Math.floor((y - 4) * 30.0 / 33.0);
+ value = v;
+
+ this.value0 = value;
+ this.y0 = y;
+
+ } else {
+ var d = (y - this.y0) / 7.0;
+ value = this.value0 + d;
+ }
+ } else {
+ // Ctrl+Alt+ドラッグã§ã‚µã‚¤ã‚ºå¤‰æ›´ã™ã‚‹ã¨ã
+ var d = y - this.y0;
+ value = this.value0 + d;
+ }
+
+ value = Math.max(Math.min(value, 30), 1);
+ this.setSize(value);
+};
+
+Neo.SizeSlider.prototype.setSize = function(value) {
+ value = Math.round(value);
+ Neo.painter.lineWidth = Math.max(Math.min(30, value), 1);
+
+ var tool = Neo.painter.getCurrentTool();
+ if (tool) {
+ if (tool.type == Neo.Painter.TOOLTYPE_BRUSH) {
+ Neo.painter.alpha = tool.getAlpha();
+ Neo.sliders[Neo.SLIDERTYPE_ALPHA].update();
+
+ } else if (tool.type == Neo.Painter.TOOLTYPE_TEXT) {
+ Neo.painter.updateInputText();
+ }
+ }
+ this.update();
+};
+
+Neo.SizeSlider.prototype.update = function() {
+ this.value = Neo.painter.lineWidth;
+
+ var height = this.value * 33.0 / 30.0;
+ height = Math.max(Math.min(34, height), 1);
+
+ this.slider.style.height = height.toFixed(2) + "px";
+ this.label.innerHTML = this.value + "px";
+ this.slider.style.backgroundColor = Neo.painter.foregroundColor;
+};
+
+/*
+ -------------------------------------------------------------------------
+ LayerControl
+ -------------------------------------------------------------------------
+*/
+
+Neo.LayerControl = function() {};
+Neo.LayerControl.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+ this.isMouseDown = false;
+
+ var ref = this;
+
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ e.preventDefault();
+ }, true);
+
+ this.element.className = "layerControl";
+
+ var layerStrings = [Neo.translate("Layer0"),
+ Neo.translate("Layer1")];
+
+ this.element.innerHTML =
+ "<div class='bg'></div><div class='label0'>" + layerStrings[0] +
+ "</div><div class='label1'>" + layerStrings[1] +
+ "</div><div class='line1'></div><div class='line0'></div>";
+
+ this.bg = this.element.getElementsByClassName('bg')[0];
+ this.label0 = this.element.getElementsByClassName('label0')[0];
+ this.label1 = this.element.getElementsByClassName('label1')[0];
+ this.line0 = this.element.getElementsByClassName('line0')[0];
+ this.line1 = this.element.getElementsByClassName('line1')[0];
+
+ this.line0.style.display = "none";
+ this.line1.style.display = "none";
+ this.label1.style.display = "none";
+
+ this.update();
+ return this;
+};
+
+Neo.LayerControl.prototype._mouseDownHandler = function(e) {
+ if (Neo.getModifier(e) == "right") {
+ var visible = Neo.painter.visible[Neo.painter.current];
+ Neo.painter.visible[Neo.painter.current] = (visible) ? false : true;
+
+ } else {
+ var current = Neo.painter.current;
+ Neo.painter.current = (current) ? 0 : 1
+ }
+ Neo.painter.updateDestCanvas(0, 0, Neo.painter.canvasWidth, Neo.painter.canvasHeight);
+ if (Neo.painter.tool.type == Neo.Painter.TOOLTYPE_PASTE) {
+ Neo.painter.tool.drawCursor(Neo.painter);
+ }
+ this.update();
+
+ if (this.onmousedown) this.onmousedown(this);
+};
+
+Neo.LayerControl.prototype.update = function() {
+ this.label0.style.display = (Neo.painter.current == 0) ? "block" : "none";
+ this.label1.style.display = (Neo.painter.current == 1) ? "block" : "none";
+ this.line0.style.display = (Neo.painter.visible[0]) ? "none" : "block";
+ this.line1.style.display = (Neo.painter.visible[1]) ? "none" : "block";
+};
+
+/*
+ -------------------------------------------------------------------------
+ ReserveControl
+ -------------------------------------------------------------------------
+*/
+Neo.reserveControls = [];
+
+Neo.ReserveControl = function() {};
+Neo.ReserveControl.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ var ref = this;
+
+ this.element.onmousedown = function(e) { ref._mouseDownHandler(e); }
+ this.element.addEventListener("touchstart", function(e) {
+ ref._mouseDownHandler(e);
+ e.preventDefault();
+ }, true);
+
+ this.element.className = "reserve";
+
+ var index = parseInt(this.name.slice(7)) - 1;
+ this.element.style.top = "1px";
+ this.element.style.left = (index * 15 + 2) + "px";
+
+ this.reserve = Neo.clone(Neo.config.reserves[index]);
+ this.update();
+
+ Neo.reserveControls.push(this);
+ return this;
+};
+
+Neo.ReserveControl.prototype._mouseDownHandler = function(e) {
+ if (Neo.getModifier(e) == 'right') {
+ this.save();
+ } else {
+ this.load();
+ }
+ this.update();
+};
+
+Neo.ReserveControl.prototype.load = function() {
+ Neo.painter.setToolByType(this.reserve.tool)
+ Neo.painter.foregroundColor = this.reserve.color;
+ Neo.painter.lineWidth = this.reserve.size;
+ Neo.painter.alpha = this.reserve.alpha;
+
+ switch (this.reserve.tool) {
+ case Neo.Painter.TOOLTYPE_PEN:
+ case Neo.Painter.TOOLTYPE_BRUSH:
+ case Neo.Painter.TOOLTYPE_TONE:
+ Neo.painter.drawType = this.reserve.drawType;
+ };
+ Neo.updateUI();
+};
+
+Neo.ReserveControl.prototype.save = function() {
+ this.reserve.color = Neo.painter.foregroundColor;
+ this.reserve.size = Neo.painter.lineWidth;
+ this.reserve.drawType = Neo.painter.drawType;
+ this.reserve.alpha = Neo.painter.alpha;
+ this.reserve.tool = Neo.painter.tool.getType();
+ this.element.style.backgroundColor = this.reserve.color;
+ this.update();
+ Neo.updateUI();
+};
+
+Neo.ReserveControl.prototype.update = function() {
+ this.element.style.backgroundColor = this.reserve.color;
+};
+
+/*
+ -------------------------------------------------------------------------
+ ScrollBarButton
+ -------------------------------------------------------------------------
+*/
+
+Neo.scrollH;
+Neo.scrollV;
+
+Neo.ScrollBarButton = function() {};
+Neo.ScrollBarButton.prototype.init = function(name, params) {
+ this.element = document.getElementById(name);
+ this.params = params || {};
+ this.name = name;
+
+ this.element.innerHTML = "<div></div>";
+ this.barButton = this.element.getElementsByTagName("div")[0];
+ this.element['data-bar'] = true;
+ this.barButton['data-bar'] = true;
+
+ if (name == "scrollH") Neo.scrollH = this;
+ if (name == "scrollV") Neo.scrollV = this;
+ return this;
+};
+
+Neo.ScrollBarButton.prototype.update = function(oe) {
+ if (this.name == "scrollH") {
+ var a = oe.destCanvas.width / (oe.canvasWidth * oe.zoom);
+ var barWidth = Math.ceil(oe.destCanvas.width * a);
+ var barX = (oe.scrollBarX) * (oe.destCanvas.width - barWidth);
+ this.barButton.style.width = (Math.ceil(barWidth) - 4) + "px";
+ this.barButton.style.left = Math.floor(barX) + "px";
+
+ } else {
+ var a = oe.destCanvas.height / (oe.canvasHeight * oe.zoom);
+ var barHeight = Math.ceil(oe.destCanvas.height * a);
+ var barY = (oe.scrollBarY) * (oe.destCanvas.height - barHeight);
+ this.barButton.style.height = (Math.ceil(barHeight) - 4) + "px";
+ this.barButton.style.top = Math.floor(barY) + "px";
+ }
+};
+
diff --git a/static/js/palette_selfy.js b/static/js/palette_selfy.js
new file mode 100755
index 0000000..b17134f
--- /dev/null
+++ b/static/js/palette_selfy.js
@@ -0,0 +1,972 @@
+// palette_selfy.js .. for PaintBBS and ShiPainter .. last update : 2004/04/11.
+
+//¨Žg‚¢•û .. ŠO•”JS‚Æ‚µ‚Ä“Ç‚Ýž‚ñ‚Å‚©‚çAD‚«‚ÈŠ‚Å palette_selfy() ‚ðŒÄ‚Ño‚µ‚ĉº‚³‚¢.
+ var selfv=new Array(); var selfytag=new Array(); //©Á‚³‚È‚¢‚Å.
+
+//«Ý’è ------------------------------------------------------------
+// ¦selfv[*] ‚ÍA‚»‚ꂼ‚ê‚ÌÝ’è‚ð‹ó”’‚É‚·‚é‚ÆA‚»‚Ì‹@”\‚̃{ƒ^ƒ“‚ð•\Ž¦‚³‚¹‚È‚­‚Å‚«‚Ü‚·.
+
+// +-‚·‚é’l‚Ì‚Æ‚±
+var pnum = 10; // +- ‚̃fƒtƒHƒ‹ƒg’l
+selfv[0] = 'size=3 style="text-align:right">'; // ”’lƒ^ƒO(type=text)‚Ì’†g
+
+
+// ƒpƒŒƒbƒgƒŠƒXƒg.
+// ..Še—v‘f‚Ì’†‚ÌF‚ÍA1‚‚¾‚¯‚Ȃ瑼‚Ì13F‚Í‚»‚ÌF‚ðŒ³‚É«‚Ì2’Ê‚è‚©‚玩“®Žæ“¾A
+var psx = 0; // 0:Ê“x+–¾“x‚ðŒ³‚É. 1:F‘Š‚ðzŠÂ‚Å.
+
+// Ê“x+–¾“x‚ðŒ³‚É‚·‚é‚Æ‚«‚ÌF. (•¡”Ý’è‚·‚éꇂÍA1‚Â1‚‚ÌF‚ð \n ‚Å‹æØ‚é)
+var pdefs = new Array(
+ '#ffffff',
+ '#ffe6e6','#ffece6','#fff3e6','#fff9e6','#ffffe6',
+ '#f3ffe6','#e6fff3','#e6f3ff','#ffe6ff','#eeddbb',
+''); // ¦‹ó”’‚¾‚Æ‚»‚Ì—v‘f‚̓XƒLƒbƒv.
+
+// F‘Š‚ÅzŠÂ‚³‚¹‚é‚Æ‚«‚ÌF. (•¡”Ý’è‚·‚éꇂÍA1‚Â1‚‚ÌF‚ð \n ‚Å‹æØ‚é)
+var pdefx = new Array(
+ '#ffffff',
+ '#ffe6e6','#ffcccc','#ff9999','#e6cccc','#e69999',
+ '#cc9999','#cc6666','#996666','#993333','#660000',
+''); // ¦‹ó”’‚¾‚Æ‚»‚Ì—v‘f‚̓XƒLƒbƒv.
+
+
+// ƒfƒtƒHƒ‹ƒg‚̃pƒŒƒbƒgƒJƒ‰[ (ˆê”Ôʼn‚ɃAƒvƒŒƒbƒg‚É‚Å‚éF)
+var pbase = '#000000\n#FFFFFF\n#B47575\n#888888\n#FA9696\n#C096C0\n#FFB6FF\n#8080FF\n#25C7C9\n#E7E58D\n#E7962D\n#99CB7B\n#FCECE2\n#F9DDCF';
+
+
+// ƒTƒ“ƒvƒ‹ƒJƒ‰[‚Ì
+ // •\Ž¦‚·‚éƒpƒŒƒbƒg‚̃Jƒ‰[”Ô†(‚±‚Ì’†‚É‚ ‚é”Ô†‚¾‚¯«‚Å‘‚«o‚µ)
+var sams = new Array(0,2,4,6,8,10,12,1,3,5,7,9,11,13); // Ê“x+–¾“x‚ðŒ³‚É‚·‚é‚Æ‚«
+var samx = new Array(0,1,2,3,4,5,6,7,8,9,10,11,12,13); // F‘Š‚ÅzŠÂ‚³‚¹‚é‚Æ‚«
+
+selfv[1] = '&nbsp;'; // ƒtƒHƒ“ƒg
+selfv[2] = 'style="font-size:xx-small; background-color:$FONT;"';
+ // ƒtƒHƒ“ƒgƒ^ƒO‚Ì’†g(u$FONTv:16i–@RGBF‚ª‘ã“üA³Šm‚É“K—p‚³‚ê‚é‘®«‚Í«‚Ì3‚Â)
+ // c color, style="color" style="background", style="background-color")
+
+ // ¦«‚±‚ê‚æ‚艺‚Í ">"(•Â‚¶ƒ^ƒO) ‚à“ü‚ê‚Ä‚­‚¾‚³‚¢ //
+
+// ƒpƒŒƒbƒg‚Ì‘I‘ðƒ{ƒ^ƒ“(type=radio)ƒ^ƒO‚Ì’†g
+selfv[3] = 'style="border-width:0;" title="ƒfƒtƒHƒ‹ƒg‚̃pƒŒƒbƒg">'; // ƒfƒtƒHƒ‹ƒgF
+selfv[4] = 'style="border-width:0;" title="‚±‚±‚̃pƒŒƒbƒg‚ðŽg‚¤B\nƒ`ƒFƒbƒN‚µ‚Ä‚é‚Æ‚«‚É‚³‚ç‚ɉŸ‚·‚ÆAƒ`ƒFƒbƒN‚ªŠO‚êA\nF‘ŠzŠÂ‚©AÊ“x+–¾“xƒpƒŒƒbƒg‚ÉB(1”Ô‚ÌF‚ªŠî–{F)">'; // ‘I‘ð
+
+
+// ƒ{ƒ^ƒ“(type=button)ƒ^ƒO‚Ì’†g
+selfv[5] = 'value="H" title="F‘ŠƒpƒŒƒbƒg (1”Ô‚ÌF‚ªŠî–{F)">'; // F‘Š
+selfv[6] = 'value="S" title="Ê“xƒpƒŒƒbƒg (1”Ô‚ÌF‚ªŠî–{F)">'; // Ê“x
+selfv[7] = 'value="B" title="–¾“xƒpƒŒƒbƒg (1”Ô‚ÌF‚ªŠî–{F)">\n'; // –¾“x
+selfv[8] = 'value="o" title="‚±‚±‚É¡‚̃pƒŒƒbƒg‚ð•Û‘¶">'; // ƒZ[ƒu
+selfv[9] = 'value="x" title="‚±‚±‚̃pƒŒƒbƒg‚ðƒfƒtƒHƒ‹ƒg‚É–ß‚·"><br>\n'; // ƒfƒtƒHƒ‹ƒg
+
+selfv[10] = 'value="H+" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌF‘Š‚ð{">'; // F‘Š+
+selfv[11] = 'value="H-" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌF‘Š‚ð|">'; // F‘Š-
+selfv[12] = 'value="S+" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌÊ“x‚ð{">'; // Ê“x+
+selfv[13] = 'value="S-" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌÊ“x‚ð|">'; // Ê“x-
+selfv[14] = 'value="B+" title="ƒpƒŒƒbƒg‘S‘Ì‚Ì–¾“x‚ð{">\n'; // –¾“x+
+selfv[15] = 'value="B-" title="ƒpƒŒƒbƒg‘S‘Ì‚Ì–¾“x‚ð|">\n'; // –¾“x-
+selfv[16] = 'value="RGB+" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌRGB‚ð{"><br>\n'; // RGB+
+selfv[17] = 'value="RGB-" title="ƒpƒŒƒbƒg‘S‘Ì‚ÌRGB‚ð|"><br>\n'; // RGB-
+
+
+// ƒOƒ‰ƒf[ƒVƒ‡ƒ“‚Ì‚Æ‚±
+selfv[18] = 'style="border-width:0;" title="2“_‚ðŒ³‚ÉƒOƒ‰ƒf[ƒVƒ‡ƒ“ (1”Ô‚ÌF‚Æ14”Ô‚ÌF)" checked>2'; // 2“_
+selfv[19] = 'style="border-width:0;" title="3“_‚ðŒ³‚ÉƒOƒ‰ƒf[ƒVƒ‡ƒ“ (1”ÔA8”ÔA14”Ô‚ÌF)">3'; // 3“_
+selfv[20] = 'style="border-width:0;" title="4“_‚ðŒ³‚ÉƒOƒ‰ƒf[ƒVƒ‡ƒ“ (1A6A10A14”Ô‚ÌF)">4<br>\n'; // 4“_
+selfv[21] = 'value="RGB" title="RGB‚ŃOƒ‰ƒf[ƒVƒ‡ƒ“">\n'; // RGB?
+selfv[22] = 'value="+HSB" title="+HSB‚ŃOƒ‰ƒf[ƒVƒ‡ƒ“ (F‘Š{•ûŒü)">\n'; // +HSB
+selfv[23] = 'value="-HSB" title="-HSB‚ŃOƒ‰ƒf[ƒVƒ‡ƒ“ (F‘Š|•ûŒü)"><br>\n'; // -HSB
+
+
+// ’ljÁEíœ
+selfv[24] = 'value="+" title="ƒpƒŒƒbƒg‚ð’ljÁ‚µ‚Ü‚·">'; // ’ljÁ
+selfv[25] = 'value="-" title="‘I‘𒆂̃pƒŒƒbƒg‚ð휂µ‚Ü‚·">\n'; // íœ
+
+
+// ƒZ[ƒuEƒI[ƒgƒZ[ƒu
+selfv[26] = 'checked title="‚±‚±‚Ƀ`ƒFƒbƒN‚ð‚‚¯‚Ä‚¨‚­‚ÆAF‚ð•ÏX‚·‚é‚Æ‚«A\n@Ž©“®‚Å•Û‘¶ƒpƒŒƒbƒg‚ɃpƒŒƒbƒgî•ñ‚ðƒZ[ƒu‚µ‚Ü‚·B\nŽ©“®•Û‘¶‚ª“K—p‚³‚ê‚é‚Ì‚ÍA\n@ƒ`ƒFƒbƒN‚µ‚Ä‚éƒpƒŒƒbƒg‚©‚瑼‚̃pƒŒƒbƒg‚Ɉړ®‚µ‚½‚Æ‚«A\n@ƒ`ƒFƒbƒN‚µ‚Ä‚éƒpƒŒƒbƒg‚ÌH/S/Bƒ{ƒ^ƒ“‚ð‰Ÿ‚µ‚½‚Æ‚«A\n@‚Ì2‚‚̂Ƃ«‚Å‚·BƒTƒ“ƒvƒ‹‚ÌF‚à•Ï‚í‚è‚Ü‚·B\n‚à‚¿‚ë‚ñA‚±‚±‚Ƀ`ƒFƒbƒN‚µ‚Ä‚È‚­‚Ä‚àA\n@Žè“®‚ŃZ[ƒuƒ{ƒ^ƒ“‚ð‰Ÿ‚¹‚ÎAƒpƒŒƒbƒg‚É•Û‘¶‚³‚ê‚Ü‚·B">'; // Ž©“®ƒZ[ƒu
+selfv[27] = 'value="O" title="¡‚Ì‘S‘̂̃pƒŒƒbƒg‚ðƒNƒbƒL[‚É•Û‘¶"><br>\n'; // ƒZ[ƒu
+
+
+// ƒfƒtƒHƒ‹ƒg‚̃pƒŒƒbƒg‚ð F‘Š360‹‚É‚·‚é‚©AÊ“x++–¾“x-- ‚É‚·‚é‚©
+selfv[28] = 'style="border-width:0;" title="ƒfƒtƒHƒ‹ƒg‚̃pƒŒƒbƒg‚ÍAF‘Š‚ÅzŠÂ‚³‚¹‚é">H<sup>o</sup>'; // H‹
+selfv[29] = 'style="border-width:0;" title="ƒfƒtƒHƒ‹ƒg‚̃pƒŒƒbƒg‚ÍAÊ“x{A–¾“x|‚ŃŠƒXƒg">+S-B'; // +S-B
+selfv[30] = 'value="X" title="‘S‘̂̃pƒŒƒbƒg‚ðƒfƒtƒHƒ‹ƒg‚É–ß‚·"><br>\n'; // ƒfƒtƒHƒ‹ƒg‚É
+
+
+// UPLOAD / DOWNLOAD
+selfv[31] = 'value="" size=8 title="ƒpƒŒƒbƒgƒf[ƒ^B\nEƒAƒbƒvƒ[ƒh‚·‚é‚Æ‚«‚ÍA‚±‚±‚É“\‚è•t‚¯‚Ä‚­‚¾‚³‚¢B\nEƒ_ƒEƒ“ƒ[ƒh‚·‚é‚Æ‚«‚ÍA‚±‚±‚Ƀf[ƒ^‚ªo—Í‚³‚ê‚Ü‚·B\n@ ƒ[ƒJƒ‹‚̃eƒLƒXƒg‚É‚Å‚à•Û‘¶‚µ‚Ä‚­‚¾‚³‚¢B\n¦ƒpƒŒƒbƒgƒf[ƒ^‚ÍA\n pals = new Array(\'#FFFFFF\',\'#B47575\\n#888888\\n...\');\n@‚̂悤‚ÉAJS‚Ì”z—ñŒ`Ž®‚Å‘‚©‚ê‚Ü‚·B">\n'; //
+selfv[32] = 'value="ª" title="©‚©‚çƒpƒŒƒbƒgƒf[ƒ^‚ðƒAƒbƒvƒ[ƒh">'; //
+selfv[33] = 'value="«" title="©©‚ɃpƒŒƒbƒgƒf[ƒ^‚ðƒ_ƒEƒ“ƒ[ƒh"><br>\n'; //
+
+
+// ‚±‚̃pƒŒƒbƒgƒe[ƒuƒ‹‚ðˆÍ‚ñ‚Å‚éAƒ\[ƒX‚Æ‚©ƒ^ƒO‚Æ‚© (formƒ^ƒO‚ÍÁ‚µ‚¿‚áƒ_ƒ)
+// ƒtƒH[ƒ€Žn‚Ü‚è
+selfytag[0] = '<table class="ptable"><tr><form name="palepale"><td class="ptd" nowrap>\n<div align=right class="menu">Palette-Selfy</div>\n<div style="font-size:xx-small;">\n';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_1 (ŒÂ•Ê‚̃pƒŒƒbƒg ` ‘S‘Ì‚Ì HSBARGB +- ‚Æ‚©)
+selfytag[1] = '<div style="text-align:right; padding:5;">\n';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_2 (‘S‘Ì‚Ì HSBARGB +- ‚Æ‚© ` ƒOƒ‰ƒf[ƒVƒ‡ƒ“)
+selfytag[2] = '</div>\n<div style="text-align:right; padding:0 5 5 5;"">\nGradation';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_3 (ƒOƒ‰ƒf[ƒVƒ‡ƒ“ ` ƒpƒŒƒbƒg‚̒ljÁE휃{ƒ^ƒ“)
+selfytag[3] = '</div>\n<div style="text-align:right; padding:0 5 0 5;"">\nPalette';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_4 (ƒpƒŒƒbƒg‚̒ljÁE휃{ƒ^ƒ“ ` ƒZ[ƒuƒ{ƒ^ƒ“)
+selfytag[4] = '\nSave';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_5 (ƒZ[ƒuƒ{ƒ^ƒ“ ` Default‚Í H++/+S-B ‚Ç‚¿‚ç‚©)
+selfytag[5] = '</div>\n<div style="text-align:right; padding:3 5 2 5;"">\nDefault';
+
+// ƒtƒH[ƒ€‚ÌŠÔ_6 (Default‚Í H++/+S-B ‚Ç‚¿‚ç‚© ` ƒpƒŒƒbƒg‚̃Aƒbƒv/ƒ_ƒEƒ“ƒ[ƒh)
+selfytag[6] = '</div>\n<div style="text-align:right; padding:0 5 0 5;">\nUpdata ';
+
+// ƒtƒH[ƒ€I‚í‚è
+selfytag[7] = '</div>\n</div>\n</td></form></tr></table>\n';
+//ªÝ’肨‚í‚è ------------------------------------------------------
+
+
+// ‰Šú’l (‚¢‚½‚é‚Æ‚±‚ë‚ÅŽg‚¤’l)
+var d = document;
+var pon,pno; // radioƒ`ƒFƒbƒN’†H / ƒ`ƒFƒbƒN‚µ‚½ƒpƒŒƒbƒgNO.
+var qon,qno,qmo; // buttonƒvƒbƒVƒ…’†H / ƒvƒbƒVƒ…‚µ‚½ƒpƒŒƒbƒgNO.
+var pals = new Array(); // color-palette
+var inp = '<input type="button" '; // input-button
+var inr = '<input type="radio" '; // input-button
+var cname = 'selfy='; // cookie-name
+var psx_ch = new Array('',''); // h_sb-checked
+var brwz=0;
+if(d.all){ brwz=1; }else if(d.getElementById){ brwz=2; }
+
+
+// -------------------------------------------------------------------------
+// HSB¨RGB ŒvŽZ. ’l‚Í0`255.
+function HSBtoRGB(h,s,v){
+ var r,g,b;
+ if(s==0){
+ r=v; g=v; b=v;
+ }else{
+ var max,min,dif;
+ h*=360/255; //360‹
+ max=v;
+ dif=v*s/255; //s=(dif/max)*255
+ min=v-dif; //min=max-dif
+
+ if(h<60){
+ r=max; b=min; g=h*dif/60+min;
+ }else if(h<120){
+ g=max; b=min; r=-(h-120)*dif/60+min;
+ }else if(h<180){
+ g=max; r=min; b= (h-120)*dif/60+min;
+ }else if(h<240){
+ b=max; r=min; g=-(h-240)*dif/60+min;
+ }else if(h<300){
+ b=max; g=min; r= (h-240)*dif/60+min;
+ }else if(h<=360){
+ r=max; g=min; b=-(h-360)*dif/60+min;
+ }else{r=0;g=0;b=0;}
+ }
+ return(new Array(r,g,b));
+}
+
+
+// RGB¨HSB ŒvŽZ. ’l‚Í0`255.
+function RGBtoHSB(r,g,b){
+ var max,min,dif,h,s,v;
+
+ // max
+ if(r>=g && r>=b){
+ max=r;
+ }else if(g>=b){
+ max=g;
+ }else{
+ max=b;
+ }
+
+ // min
+ if(r<=g && r<=b){
+ min=r;
+ }else if(g<=b){
+ min=g;
+ }else{
+ min=b;
+ }
+
+ // 0,0,0
+ if(max<=0){ return(new Array(0,0,0)); }
+
+ // difference
+ dif=max-min;
+
+ //Hue:
+ if(max>min){
+ if(g==max){
+ h=(b-r)/dif*60+120;
+ }else if(b==max){
+ h=(r-g)/dif*60+240;
+ }else if(b>g){
+ h=(g-b)/dif*60+360;
+ }else{
+ h=(g-b)/dif*60;
+ }
+ if(h<0){
+ h=h+360;
+ }
+ }else{ h=0; }
+ h*=255/360;
+
+ //Saturation:
+ s=(dif/max)*255;
+
+ //Value:
+ v=max;
+
+ return(new Array(h,s,v));
+}
+
+
+// RGB16¨RGB10 •\‹L. ’l‚Í 000000`ffffff
+function to10rgb(str){
+ var ns = new Array();
+ str = str.replace(/[^0-9a-fA-F]/g,'');
+ for(var i=0; i<=2; i++){
+ ns[i] = str.substr(i*2,2);
+ if(!ns[i]){ ns[i]='00'; }
+ ns[i] = Number(parseInt(ns[i],16).toString(10));
+ }
+ return(ns);
+}
+
+
+// 10¨16i–@“ñŒ…
+function format16(n){
+ n = Number(n).toString(16);
+ if(n.length<2){ n='0'+n; }
+ return(n);
+}
+
+
+
+
+// -------------------------------------------------------------------------
+// ƒpƒŒƒbƒg‚É (¦ q=1:ƒAƒvƒŒƒbƒgƒpƒŒƒbƒg‚Éo—Í‚µ‚È‚¢. lst=1:ʼn‚Ì‚Æ‚«
+function rady(p,q,lst){
+ var d = document;
+ var df = d.forms.palepale;
+
+ // ƒfƒtƒHƒ‹ƒgƒpƒŒƒbƒg
+ if(!p&&p!=0){ pon=0; pno=''; d.paintbbs.setColors(pbase); return; }
+
+ var ps = pals[p].split('\n');
+ var n = pnum;
+ if(!q && df.num.value){ n = Number(df.num.value); }
+ if(!q && pon==1 && pno!=p){ poncheck(); }
+
+ // ‘µ‚Á‚Ä‚é‚È‚ç‚·‚®•Ô‚·
+ if((pon!=1 || pno!=p) && ps.length==14){
+ if(!q){ pon=1; pno=p; }
+ if(q!=1 && pals[p]){ d.paintbbs.setColors(pals[p]); } return;
+ }
+
+ // check‚µ‚Ä‚é‚È‚ç
+ if(pon==1 && pno==p){
+ var pget = String(d.paintbbs.getColors());
+// if(pget==pals[p]){ return; }
+ var cs = pget.split('\n');
+ ps[0] = cs[0]; ps[1] = '';
+ }
+ // Œ‡‚¯‚Ä‚¢‚é‚È‚ç
+ var cs = new Array();
+
+ var psy=0; // H‹/ +S-B
+ psy = check_h_sb(lst);
+
+ if(psy==1){ cs = rh_list(p,n); }// H‹ƒŠƒXƒg
+ else{ cs = sb_list(p,n); } // +S-B ƒŠƒXƒg
+
+ if(q){ // ‰Šúݒ莞
+ pals[p] = String(cs.join('\n'));
+ }
+ if(q!=1){ // ˆê”Ê
+ if(pon==1 && pno==p){ checkout(); }
+ else{ pon=1; pno=p; }
+// pals[p] = String(cs.join('\n'));
+ d.paintbbs.setColors(String(cs.join('\n')));
+ }
+}
+
+
+// H‹ƒŠƒXƒg
+function rh_list(p,n){
+ var ps = pals[p].split('\n');
+ var rgb = to10rgb(ps[0]); //¨RGB
+ var hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ var cs = new Array(ps[0],ps[1]);
+ if(!cs[0]){ cs[0]='#ffffff'; }
+ if(hsv[1]!=0 && !cs[13]){ cs[13]='#ffffff'; }
+
+ for (var i=1; i<13; i++){
+ if(ps[i] && (pon!=1 || pno!=p)){ cs[i]=ps[i]; continue; } //‚ ‚é
+ var x,y,z;
+ if(hsv[1]==0){ //”’•
+ x = hsv[0];
+ y = 0;
+ if(i%2==0){ z = 255-i*n; }else{ z = 0+(i-1)*n; }
+ }else if(i>=12){
+ x = hsv[0];
+ y = 0;
+ z = 255-hsv[1];
+ }else{
+ x = hsv[0] + i*255/12;
+ y = hsv[1];
+ z = hsv[2];
+ }
+ while(x<0){ x+=255; } if(y<0){ y=0; } if(z<0){ z=0; } //«0
+ while(x>255){ x-=255; } if(y>255){ y=255; } if(z>255){ z=255; } //ª255
+// for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ rgb = HSBtoRGB(x,y,z);
+ for (var j=0; j<=2; j++){ rgb[j] = Math.round(rgb[j]); }
+ cs[i] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ return(cs);
+}
+
+
+// +S-B ƒŠƒXƒg
+function sb_list(p,n){
+ var ps = pals[p].split('\n');
+ var rgb = to10rgb(ps[0]); //¨RGB
+ var hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ var cs = new Array(ps[0],ps[1]);
+ if(!cs[0]){ cs[0]='#ffffff'; }
+ if(hsv[1]==0 && !cs[1]){ cs[1]='#000000'; }
+ else if(!cs[1]){ cs[1]='#ffffff'; }
+
+ for (var i=2; i<14; i++){
+ if(ps[i] && (pon!=1 || pno!=p)){ cs[i]=ps[i]; continue; } //‚ ‚é
+ var y,z;
+ if(hsv[1]==0){ //”’•
+ y = 0;
+ if(i%2==0){ z = 255-i*n; }else{ z = 0+(i-1)*n; }
+ }else{
+ if(i%2==0){ //¶
+ y = hsv[1]+i*n;
+ z = hsv[2];
+ }else{ //‰E
+ y = hsv[1]+(i-1)*n;
+ z = hsv[2]-(i-1)*n;
+ }
+ }
+ while(z<0){ z+=255; } while(y<0){ y+=255; } //«0
+ while(z>255){ z-=255; } while(y>255){ y-=255; } //ª255
+// for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ rgb = HSBtoRGB(hsv[0],y,z);
+ for (var j=0; j<=2; j++){ rgb[j] = Math.round(rgb[j]); }
+ cs[i] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ return(cs);
+}
+
+
+// ŒÂ•Ê‚ÅH/S/B‚ðƒŠƒXƒgƒAƒbƒv
+function onplus(p,m){
+ var d = document;
+ var df = d.forms.palepale;
+ var n = Number(df.num.value); //+-
+ if(pon==1 && pno==p){ poncheck(); }
+
+ // ˜A‘±‚Ì‚Æ‚«
+ if(m>0 && n*(qon+1)>38){ qon=0; }
+ if(qno==p && qmo==m && qon>=1){ qon++; n*=(qon+1)/2; }
+ else{ qno=p; qmo=m; qon=1; }
+
+ var ps = pals[p].split('\n');
+ var rgb = to10rgb(ps[0]); //¨RGB
+ var hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ var cs = new Array();
+ if(m==2){ n*=-1; }
+ for (var i=0; i<14; i++){
+ var z;
+ if(m==0){ z = hsv[m]+((i%2)*2-1)*Math.round(Math.floor(i/2)*(n)); }
+ else{ z = hsv[m]+i*n; }
+ while(z<0){ z+=255; } //«0
+ while(z>255){ z-=255; } //ª255
+// for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ if(m==1){ rgb = HSBtoRGB(hsv[0],z,hsv[2]); } //¨HSB
+ else if(m==2){ rgb = HSBtoRGB(hsv[0],hsv[1],z); }
+ else{ rgb = HSBtoRGB(z,hsv[1],hsv[2]); } //¨HSB
+ for (var j=0; j<=2; j++){ rgb[j] = Math.round(rgb[j]); }
+ cs[i] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ checkout(1);
+ d.paintbbs.setColors(String(cs.join('\n')));
+}
+
+
+// ‘S‘Ì‚ÌH/S/B‚ðƒvƒ‰ƒXƒ}ƒCƒiƒX
+function alplus(m,n){
+ var d = document;
+ var cs = String(d.paintbbs.getColors()).split('\n');
+ n *= Number(d.forms.palepale.num.value); //+-
+ poncheck();
+
+ for (var i=0; i<cs.length; i++){
+ var rgb = to10rgb(cs[i]); //¨RGB
+ var hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ //ª–¾“x255‚Ì‚Æ‚«Ê“xŒ¸
+ if(m==2 && n>0 && hsv[2]>=255){
+ hsv[1] -= n;
+ if(hsv[1]<0){ hsv[1]=0; }else if(hsv[1]>255){ hsv[1]=255; } //«0 or ª255
+ }
+ hsv[m] += n;
+ //«0 ª255
+ if(m==0){
+ if(hsv[0]<0){ hsv[0]+=255; }else if(hsv[0]>255){ hsv[0]-=255; }
+ }else{
+ if(hsv[m]<0){ hsv[m]=0; }else if(hsv[m]>255){ hsv[m]=255; }
+ }
+// for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ rgb = HSBtoRGB(hsv[0],hsv[1],hsv[2]); //¨HSB
+ for (var j=0; j<=2; j++){ rgb[j] = Math.round(rgb[j]); }
+ cs[i] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ checkout();
+ d.paintbbs.setColors(String(cs.join('\n')));
+}
+
+
+// ‘S‘Ì‚ÌRGB‚ðƒvƒ‰ƒXƒ}ƒCƒiƒX
+function alrgb(n){
+ var d = document;
+ var cs = String(d.paintbbs.getColors()).split('\n');
+ n *= Number(d.forms.palepale.num.value); //+-
+ poncheck();
+
+ for (var i=0; i<cs.length; i++){
+ var rgb = to10rgb(cs[i]); //¨RGB
+ for (var j=0; j<=2; j++){
+ rgb[j] += n;
+ rgb[j] = Math.round(rgb[j]);
+ if(rgb[j]<0){ rgb[j]=0; } //«0
+ if(rgb[j]>255){ rgb[j]=255; } //ª255
+ }
+ cs[i] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ checkout();
+ d.paintbbs.setColors(String(cs.join('\n')));
+}
+
+
+// ƒOƒ‰ƒf[ƒVƒ‡ƒ“
+function grady(m){
+ var d = document;
+ var df = d.forms.palepale;
+ var n = 2;
+ if(df.gradc){
+ for(var j=0; j<df.gradc.length; j++){
+ if(df.gradc[j].checked == true){ n = Number(df.gradc[j].value); break; }
+ }
+ }
+ var cs = String(d.paintbbs.getColors()).split('\n');
+ var gs = new Array(1,13);
+ if(n==3){ gs = new Array(1,7,13); }
+ else if(n==4){ gs = new Array(1,5,9,13); }
+ poncheck();
+ cs[1] = cs[0];
+
+ // 2`4F
+ for (var i=0; i<gs.length-1; i++){
+ var p=gs[i]; var q=gs[(i+1)];
+ var rgbp = to10rgb(cs[p]); //¨RGB
+ var rgbq = to10rgb(cs[q]); //¨RGB2
+ // HSB
+ var hsvp = new Array();
+ var hsvq = new Array();
+ if(m==1 || m==-1){
+ hsvp = RGBtoHSB(rgbp[0],rgbp[1],rgbp[2]); //¨HSB
+ hsvq = RGBtoHSB(rgbq[0],rgbq[1],rgbq[2]); //¨HSB
+ }
+ // ƒpƒŒƒbƒg‚ÌF
+ for (var k=p+1; k<q; k++){
+ var rgb = new Array();
+ // HSB
+ if(m==1 || m==-1){
+ var hsv = new Array();
+ for (var j=0; j<=2; j++){ // RGB
+ var sa = (hsvp[j]-hsvq[j])/(q-p);
+ if(j==0){ // H
+ if(m*hsvp[j]>m*hsvq[j]){ sa = Math.abs(sa) - 255/(q-p); }
+ hsv[0] = hsvp[0] + m*Math.abs(sa)*(k-p);
+ if(hsv[0]<0){ hsv[0]+=255; }else if(hsv[0]>255){ hsv[0]-=255; }
+ }else{ // S,B
+ hsv[j] = hsvp[j] - sa*(k-p);
+ if(hsv[j]<0){ hsv[j]=0; }else if(hsv[j]>255){ hsv[j]=255; }
+ }
+ }
+ rgb = HSBtoRGB(hsv[0],hsv[1],hsv[2]); //¨HSB
+ for (var j=0; j<=2; j++){ rgb[j] = Math.round(rgb[j]); }
+ // RGB
+ }else{
+ for (var j=0; j<=2; j++){ // RGB
+ var sa = (rgbp[j]-rgbq[j])/(q-p);
+ rgb[j] = Math.round(rgbp[j] - sa*(k-p));
+ if(rgb[j]<0){ rgb[j]=0; }else if(rgb[j]>255){ rgb[j]=255; } //ª«
+ }
+ }
+ cs[k] = '#'+format16(rgb[0])+format16(rgb[1])+format16(rgb[2]);
+ }
+ }
+ cs[0]=cs[1]; cs[1]='#ffffff';
+ checkout();
+ d.paintbbs.setColors(String(cs.join('\n')));
+}
+
+
+// -------------------------------------------------------------------------
+// ƒpƒŒƒbƒg‚̃Tƒ“ƒvƒ‹ƒJƒ‰[
+function csamp(p,pz,lst){
+ var ss='';
+ var ps = pz.split('\n');
+ var slong = sams.length;
+ var psy = check_h_sb(lst); if(psy==1){ slong = samx.length; }
+ // color-sample
+ for (var i=0; i<slong; i++){
+ // color-title
+ var k,cl='',rgb='',hsv='',ctl='';
+ if(psy==1){ k=samx[i]; }else{ k=sams[i]; }
+ if(ps[k]){
+ rgb = to10rgb(ps[k]); //¨RGB
+ hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ ctl = 'HSB: '+hsv[0]+','+hsv[1]+','+hsv[2]+'\n';
+ ctl += 'RGB: '+rgb[0]+','+rgb[1]+','+rgb[2]+'\nRGB16: '+ps[k];
+ }
+ if(selfv[2]) cl=selfv[2].replace(/\$FONT/i,ps[k]);
+ if(selfv[1]) ss += '<font id="font_'+p+'_'+k+'" '+cl+' title="'+ctl+'">'+selfv[1]+'</font>';
+ }
+ return ss;
+}
+
+
+// ƒpƒŒƒbƒg‚̃ŠƒXƒg
+function palette_list(lst){
+ var d = document;
+ var ds = '';
+ for (var p=0; p<pals.length; p++){
+ if(!pals[p]){ continue; }
+ var samw = csamp(p,pals[p],lst); //ƒTƒ“ƒvƒ‹
+
+ // element
+ if(selfv[4]) ds+=inr+'name="rad" value="'+p+'" onclick="rady('+p+')" '+selfv[4]+samw+'\n';
+// ds+='<font color="'+ps[0]+'" id="font_'+p+'" title="'+ctl+'">'+samw+'</font>';
+ if(selfv[5]) ds+=inp+'onclick="onplus('+p+',0)" '+selfv[5];
+ if(selfv[6]) ds+=inp+'onclick="onplus('+p+',1)" '+selfv[6];
+ if(selfv[7]) ds+=inp+'onclick="onplus('+p+',2)" '+selfv[7];
+ if(selfv[8]) ds+=inp+'onclick="savy('+p+')" '+selfv[8];
+ if(selfv[9]) ds+=inp+'onclick="defy('+p+')" '+selfv[9];
+ }
+ return ds;
+}
+
+
+// ƒ`ƒFƒbƒN‚ð‚‚¯‚éAƒtƒHƒ“ƒgƒJƒ‰[‚̃Tƒ“ƒvƒ‹‚ð•ÏX
+function checkin(p,not){
+ qno=''; qmo=''; qon=0;
+ if(!pals[p]){ return; }
+ var d = document;
+ // font-color
+ var ps = pals[p].split('\n');
+ var slong = sams.length;
+ var psy = check_h_sb(); if(psy==1){ slong = samx.length; }
+ // color-sample
+ for (var i=0; i<slong; i++){
+ // color-title
+ var k,rgb='',hsv='',ctl='';
+ if(psy==1){ k=samx[i]; }else{ k=sams[i]; }
+ if(ps[k]){
+ rgb = to10rgb(ps[k]); //¨RGB
+ hsv = RGBtoHSB(rgb[0],rgb[1],rgb[2]); //¨HSB
+ for (var j=0; j<=2; j++){ hsv[j] = Math.round(hsv[j]); }
+ ctl = 'HSB: '+hsv[0]+','+hsv[1]+','+hsv[2]+'\n';
+ ctl += 'RGB: '+rgb[0]+','+rgb[1]+','+rgb[2]+'\nRGB16: '+ps[k];
+ }
+ // replace
+ var ds;
+ if(brwz==1){ ds = d.all('font_'+p+'_'+k); }
+ else if(brwz==2){ ds = d.getElementById('font_'+p+'_'+k); }
+ if(ds){
+ if(ds.style.background){ ds.style.background = ps[k]; }
+ if(ds.style.backgroundColor){ ds.style.backgroundColor = ps[k]; }
+ if(ds.style.color){ ds.style.color = ps[k]; }
+ if(ds.color){ ds.color = ps[k]; }
+ }
+ }
+
+ // check
+ if(not!=1){
+ var df = d.forms.palepale;
+ for(var j=0; j<df.rad.length; j++){
+ if(df.rad[j].value == p){
+ df.rad[j].checked = true; break; }
+ }
+ }
+}
+
+
+// check‚ðŠO‚·
+function checkout(q){
+ pon=0; pno='';
+ if(q!=1){ qno=''; qmo=''; qon=0; }
+ var df = document.forms.palepale;
+ for(var j=0; j<df.rad.length; j++){
+ if(df.rad[j].checked == true){
+ df.rad[j].checked = false; break; }
+ }
+}
+
+
+// ˆÈ‘O‚̃pƒŒƒbƒg‚ðŽ©“®•Û‘¶
+function poncheck(not){
+ var d = document;
+ var df = document.forms.palepale;
+ if(df.autosave&&df.autosave.checked==false){ return; }
+ else if(pon==1){
+ var pget = String(d.paintbbs.getColors());
+ if(pals[pno] != pget){
+ pals[pno] = pget;
+ checkin(pno,1);
+ if(not!=1){ pcookset(1); }
+ }
+ }
+}
+
+
+// ƒpƒŒƒbƒg‚ðƒZ[ƒu
+function savy(p){
+ var d = document;
+ pals[p] = String(d.paintbbs.getColors());
+ checkin(p);
+ pcookset(1);
+ pon=1; pno=p;
+}
+
+
+// ƒpƒŒƒbƒg‚ðƒfƒtƒHƒ‹ƒg‚É
+function defy(p){
+ checkout();
+ var q = pdefs[p];
+ var df = document.forms.palepale;
+ if(check_h_sb()==1){ q = pdefx[p]; }
+ if(q){
+ pals[p] = q;
+ rady(p,2);
+ checkin(p);
+ }else{ minsy(p); }
+}
+
+
+// ƒpƒŒƒbƒg’ljÁ
+function plusy(){
+ var d = document;
+ if(brwz==1 || brwz==2){
+ var p=pals.length;
+ var pz = String(d.paintbbs.getColors());
+ if(pz){ pals[p] = pz; }
+ else{
+ pals[p] = '#'+Number(d.paintbbs.getInfo().m.iColor).toString(16);
+ rady(p,1);
+ }
+ }
+ if(brwz==1 && d.all('palelist').innerHTML){
+ d.all('palelist').innerHTML = palette_list();
+ checkin(p);
+ }else if(brwz==2 && d.getElementById('palelist').innerHTML){
+ d.getElementById('palelist').innerHTML = palette_list();
+ checkin(p);
+ }
+}
+
+
+// ƒpƒŒƒbƒgíœ
+function minsy(p){
+ var d = document;
+ var df = d.forms.palepale;
+ if(!p&&p!=0){
+ for(var j=0; j<=df.rad.length; j++){
+ if(df.rad[j] && df.rad[j].checked==true){p=Number(df.rad[j].value); break; }
+ }
+ }
+ if((!p&&p!=0)||p<0){ return; }
+ pals[p] = '';
+ var plong = pdefs.length;
+ if(check_h_sb()==1){ plong = pdefx.length; }
+ if(p>=plong){
+ var k=0;
+ var pds = new Array(); pds = pals;
+ pals = new Array();
+ for(var j=0; j<pds.length; j++){
+ if(p!=j && pds[j]){ pals[k] = pds[j]; k++; }
+ }
+ }
+
+ if(brwz==1 && d.all('palelist').innerHTML){
+ d.all('palelist').innerHTML = palette_list();
+ }else if(brwz==2 && d.getElementById('palelist').innerHTML){
+ d.getElementById('palelist').innerHTML = palette_list();
+ }
+ checkout();
+}
+
+
+// ƒpƒŒƒbƒgƒfƒtƒHƒ‹ƒg
+function def_list(){
+ var okd = confirm("‘S‘̂̃pƒŒƒbƒg‚ðƒfƒtƒHƒ‹ƒg‚É–ß‚µ‚Ü‚·B\n‚æ‚낵‚¢‚Å‚·‚©H");
+ if(!okd){ return; }
+ var d = document;
+ var df = d.forms.palepale;
+ pals = new Array();
+ var psy = 0;
+ var plong = pdefs.length;
+ if(check_h_sb()==1){ psy=1; plong = pdefx.length; }
+ for (var p=0; p<plong; p++){
+ if(psy==1){ pals[p]=pdefx[p]; }else{ pals[p]=pdefs[p]; }
+ }
+ for (var p=0; p<pals.length; p++){ if(pals[p]){ rady(p,1); } }
+
+ if(brwz==1 && d.all('palelist').innerHTML){
+ d.all('palelist').innerHTML = palette_list();
+ }else if(brwz==2 && d.getElementById('palelist').innerHTML){
+ d.getElementById('palelist').innerHTML = palette_list();
+ }else{
+ for (var p=0; p<pals.length; p++){
+ if(pals[p]){ checkin(p,1); }
+ }
+ }
+}
+
+
+// ƒfƒtƒHƒ‹ƒg h_sb ‚̃tƒH[ƒ€‚̃`ƒFƒbƒN. H‹‚Ƀ`ƒFƒbƒN‚ª‚‚¢‚Ä‚é‚È‚ç1
+function check_h_sb(lst){
+ var ch = 0;
+ var df = document.forms.palepale;
+ if(lst!=1 && df && df.h_sb){
+ for (var i=0; i<df.h_sb.length; i++){
+ if(df.h_sb[i].value==1 && df.h_sb[i].checked==true){ ch=1; break; }
+ }
+ }else{ ch=psx; }
+ return ch;
+}
+
+
+// ƒpƒŒƒbƒgƒf[ƒ^ ƒAƒbƒvƒ[ƒh
+function pupload(){
+ var d = document;
+ var df = d.forms.palepale;
+ var qs = new Array();
+ var palx='';
+ if(df.palz){ palx = df.palz.value; }
+ if(!palx){ return; }
+ pals = new Array();
+ if(eval(palx)){}
+ else{
+ var px = palx.split(/\(|\)/);
+ var ps = px[1].split(',');
+ for (var p=0; p<ps.length; p++){
+ var q=ps[p].replace(/[^0-9a-fA-F]/g,''); pals[p] = q;
+ }
+ }
+
+ for (var p=0; p<pals.length; p++){ if(pals[p]){ rady(p,1); } }
+
+ if(brwz==1 && d.all('palelist').innerHTML){
+ d.all('palelist').innerHTML = palette_list();
+ }else if(brwz==2 && d.getElementById('palelist').innerHTML){
+ d.getElementById('palelist').innerHTML = palette_list();
+ }else{
+ for (var p=0; p<pals.length; p++){
+ if(pals[p]){ checkin(p,1); }
+ }
+ }
+}
+
+
+// ƒpƒŒƒbƒgƒf[ƒ^ ƒ_ƒEƒ“ƒ[ƒh
+function pdownload(){
+ var d = document;
+ var df = d.forms.palepale;
+ var qs = new Array();
+ for (var p=0; p<pals.length; p++){
+ qs[p] = "\'"+pals[p].replace(/\n/g,'\\n')+"\'";
+ }
+ var palx = 'pals = new Array(\n' + qs.join('\,\n') + '\n);';
+ if(df.palz){ df.palz.value = palx; }
+}
+
+
+// ‘S‘̂̃pƒŒƒbƒgî•ñ‚ðƒNƒbƒL[‚ɃZ[ƒu
+function pcookset(o){
+ var df = document.forms.palepale;
+ if(o&&df.autosave&&df.autosave.checked==false){ return; }
+ var exp=new Date();
+ exp.setTime(exp.getTime()+1000*86400*60);
+ var cs = new Array();
+ for(var i=0; i<pals.length; i++){
+ cs[i] = escape(pals[i].replace(/\n/g,'_'));
+ }
+ var cooki = '';
+ if(df.num){ cooki += df.num.value; }
+ cooki += '_'+check_h_sb()+'_%00';
+ cooki += cs.join('%00');
+ document.cookie = cname + cooki + "; expires=" + exp.toGMTString();
+}
+
+
+// ‘S‘̂̃pƒŒƒbƒgî•ñ‚ðƒNƒbƒL[‚©‚çƒ[ƒh
+function pcookget(){
+ var cooks = document.cookie.split("; ");
+ var cooki = '';
+ for (var i=0; i<cooks.length; i++){
+ if (cooks[i].substr(0,cname.length) == cname){
+ cooki = cooks[i].substr(cname.length,cooks[i].length);
+ break;
+ }
+ }
+ if(cooki){
+ var cs = cooki.split('%00');
+ pals = new Array();
+ for(var i=0; i<cs.length-1; i++){
+ pals[i] = unescape(cs[(i+1)]).replace(/\_/g,"\n");
+ }
+ if(cs[0]){
+ var ps = cs[0].split('_');
+ if(ps[0]){ pnum = ps[0]; }
+ if(ps[1]){ psx = ps[1]; }else if(!ps[1]&&ps[1]==0){ psx=0; }
+ }
+ }
+}
+
+
+// ‘Œ¸‚·‚锂ð‘‚₵‚½‚茸‚炵‚½‚è
+function num_plus(n){
+ var df = document.forms.palepale;
+ var m = Number(df.num.value); var l=n;
+ n *= Math.abs(Math.round(m/10))+1; if(n==0){ n=l; }
+ df.num.value = m+n;
+}
+
+
+// ƒg[ƒ“ƒZƒŒƒNƒg‚Ì’l‚ð}
+function tone_plus(n){
+ var df = document.forms.palepale;
+ var m = Number(df.tone.value);
+ if(m>0){ n = Math.floor(m/10 + n)*10; }
+ if(n<0){ n=0; }else if(n<5){ n=5; }else if(n>100){ n=100; }
+ df.tone.value = n;
+ tone_sel(n);
+}
+
+
+// ƒg[ƒ“ƒZƒŒƒNƒg
+function tone_sel(t){
+ var dp=document.paintbbs;
+ t = Number(t);
+ if(t==0){ dp.getInfo().m.iTT = 0; }
+ else{ dp.getInfo().m.iTT = Math.floor(t/10)+1; }
+}
+
+
+// -------------------------------------------------------------------------
+// document.write
+function palette_selfy(){
+ var d = document;
+ var df = document.forms.palepale;
+ var pzs=palette_selfy.arguments; //ƒpƒŒƒbƒgŽw’肪‚ ‚Á‚½‚Æ‚«
+
+ // browzer
+ if(brwz!=1 && brwz!=2){ return; }
+
+ // ƒpƒŒƒbƒg‚ƃpƒŒƒbƒgƒNƒbƒL[
+ var plong = pdefs.length;
+ if(psx==1){ plong = pdefx.length; }
+ for (var p=0; p<plong; p++){
+ if(psx==1){ pals[p]=pdefx[p]; }else{ pals[p]=pdefs[p]; }
+ if(pzs && pzs.length>=1){ var ok=0; //H
+ for (var q=0; q<pzs.length; q++){ if(p==pzs[q]){ ok=1; break; } }
+ if(ok!=1){ pals[p]=''; }
+ }
+ }
+ pcookget(); // cookie-get
+ psx_ch[psx] = 'checked ';
+ for (var p=0; p<pals.length; p++){ if(pals[p]){ rady(p,1,1); } }
+
+ // basic
+ d.write(selfytag[0]);
+ if(selfv[3]) d.write(inr+'name="rad" value="-1" onclick="rady()" '+selfv[3]);
+ if(pbase) d.write(csamp(-1,pbase,1));
+
+ // +-‚·‚é”
+ if(selfv[0]){
+ d.write('\n<small>&nbsp;</small>+-');
+ d.write('<input type="text" name="num" value="'+pnum+'" '+selfv[0]);
+ d.write(inp+'value="+" onclick="num_plus(1)">');
+ d.write(inp+'value="-" onclick="num_plus(-1)">\n');
+ }
+ // ƒpƒŒƒbƒgƒŠƒXƒg
+ if(pdefs||pdefx) d.write('<div id="palelist">\n'+palette_list(1)+'</div>\n');
+
+ // ‘S‘Ì‚Ì HSBARGB +-
+ if(selfytag[1]) d.write(selfytag[1]);
+ if(selfv[10]) d.write(inp+'onclick="alplus(0,1)" ' +selfv[10]);
+ if(selfv[12]) d.write(inp+'onclick="alplus(1,1)" ' +selfv[12]);
+ if(selfv[14]) d.write(inp+'onclick="alplus(2,1)" ' +selfv[14]);
+ if(selfv[16]) d.write(inp+'onclick="alrgb(1)" ' +selfv[16]);
+ if(selfv[11]) d.write(inp+'onclick="alplus(0,-1)" '+selfv[11]);
+ if(selfv[13]) d.write(inp+'onclick="alplus(1,-1)" '+selfv[13]);
+ if(selfv[15]) d.write(inp+'onclick="alplus(2,-1)" '+selfv[15]);
+ if(selfv[17]) d.write(inp+'onclick="alrgb(-1)" ' +selfv[17]);
+
+ // ƒg[ƒ“ƒZƒŒƒNƒg
+ if(selfv[0]){
+ d.write('Tone <select name="tone" onchange="tone_sel(this.value)">');
+ for (var i=0; i<=100; i+=5){
+ d.write('<option value="'+i+'">'+i+'%</option>\n'); if(i>=10){i+=5;}
+ }
+ d.write('</select>');
+ d.write(inp+'value="+" onclick="tone_plus(1)">');
+ d.write(inp+'value="-" onclick="tone_plus(-1)">\n');
+ }
+
+ // GRADATION
+ if(selfytag[2]) d.write(selfytag[2]);
+ if(selfv[18]) d.write(inr+'name="gradc" value="2" '+selfv[18]); //18
+ if(selfv[19]) d.write(inr+'name="gradc" value="3" '+selfv[19]); //19
+ if(selfv[20]) d.write(inr+'name="gradc" value="4" '+selfv[20]); //20
+ if(selfv[21]) d.write(inp+'onclick="grady(0)" ' +selfv[21]); //21
+ if(selfv[22]) d.write(inp+'onclick="grady(1)" ' +selfv[22]); //22
+ if(selfv[23]) d.write(inp+'onclick="grady(-1)" ' +selfv[23]); //23
+
+ // ’ljÁEíœ
+ if(selfytag[3]) d.write(selfytag[3]);
+ if(selfv[24]) d.write(inp+'onclick="plusy()" ' +selfv[24]); //24
+ if(selfv[25]) d.write(inp+'onclick="minsy()" ' +selfv[25]); //25
+
+ // ƒZ[ƒuEƒI[ƒgƒZ[ƒu
+ if(selfytag[4]) d.write(selfytag[4]);
+ if(selfv[26]) d.write('<input type="checkbox" name="autosave" value="1" '+selfv[26]); //26
+ if(selfv[27]) d.write(inp+'onclick="pcookset()" ' +selfv[27]); //27
+
+ // ƒfƒtƒHƒ‹ƒg
+ if(selfytag[5]) d.write(selfytag[5]);
+ if(selfv[28]) d.write(inr+'name="h_sb" value="1" ' +psx_ch[1]+selfv[28]); //28
+ if(selfv[29]) d.write(inr+'name="h_sb" value="0" ' +psx_ch[0]+selfv[29]); //29
+ if(selfv[30]) d.write(inp+'onclick="def_list()" ' +selfv[30]); //30
+
+ // UPLOAD / DOWNLOAD
+ if(selfytag[6]) d.write(selfytag[6]);
+ if(selfv[31]) d.write('<input type="text" name="palz" '+selfv[31]); //31
+ if(selfv[32]) d.write(inp+'onclick="pupload()" ' +selfv[32]); //32
+ if(selfv[33]) d.write(inp+'onclick="pdownload()" ' +selfv[33]); //33
+
+ // /FORM
+ if(selfytag[7]) d.write(selfytag[7]);
+}
diff --git a/static/js/shobon.js b/static/js/shobon.js
new file mode 100644
index 0000000..b4e48f6
--- /dev/null
+++ b/static/js/shobon.js
@@ -0,0 +1,408 @@
+var are_filters = false;
+var hide_word = new Set()
+var hide_name = new Set();
+var hide_id = new Set();
+
+var shobon_ver = "v0.4+";
+function shobon() {
+ console.log("Running shobon " + shobon_ver);
+
+ boardName = document.getElementsByName("board")[0].value;
+ var inThread = document.getElementsByTagName("body")[0].className == "threadpage";
+ var newRepliesCounter = 0;
+
+ if(!inThread) {
+ /* Create settings link */
+ var box = document.getElementsByClassName("links")[0];
+ box.appendChild(document.createTextNode(" | "));
+ var slnk = document.createElement("a");
+ slnk.href = "#";
+ slnk.innerHTML = "<b>Configuración</b>";
+ slnk.addEventListener("click", shobonSettings);
+ box.appendChild(slnk);
+ }
+
+ if(localStorage.getItem("shobon_on") == "false") {
+ console.log("Shutting down Shobon");
+ return;
+ }
+
+ if (localStorage.getItem("shobon_usefilters") != "false") {
+ loadFilters();
+ }
+
+ var threadList = document.getElementsByClassName("thread");
+ for (var i = 0; i < threadList.length; i++) {
+ var threadId;
+ var thread = threadList[i];
+ var replyList = thread.getElementsByClassName("reply");
+ if (inThread) {
+ threadId = document.getElementsByName("parent")[0].value;
+ } else {
+ threadId = thread.getElementsByTagName("input").parent.value;
+ }
+
+ var lastReplyN = replyList[replyList.length - 1].attributes["data-n"].value;
+
+ if (localStorage.getItem(boardName + "_" + threadId) == null) {
+ localStorage.setItem(boardName + "_" + threadId, lastReplyN);
+ }
+ var lastSeen = localStorage.getItem(boardName + "_" + threadId);
+ var newRepliesInThread = 0;
+
+ for (var e = 0; e < replyList.length; e++) {
+ var reply = replyList[e];
+ var message = reply.getElementsByClassName("msg")[0];
+
+ if(localStorage.getItem("shobon_newposts") == "true") {
+ var replyId = reply.attributes["data-n"].value;
+ var isNewReply = parseInt(lastSeen) < parseInt(replyId);
+
+ if (isNewReply) {
+ newRepliesCounter++;
+ newRepliesInThread++;
+ reply.children[0].innerHTML += " <span class='shobonNew' style='color: #CC6666; font-weight: bold;'>NUEVO!</span>";
+ }
+ }
+
+ // ocultar mensajes que tienen palabras en la blacklist
+ if(are_filters) {
+ checkBlackList(reply);
+ }
+
+ // reemplaza los codigos iso de los paises por el nombre completo
+ if (localStorage.getItem("shobon_country") == "true" && boardName == "world") {
+ replaceCountryName(reply)
+ }
+ // colorea los id's
+ if(localStorage.getItem("shobon_ids") != "false") {
+ paintIds(reply);
+ }
+ // deja la barra superior fija
+ if(localStorage.getItem("shobon_navbar") == "true") {
+ fixedNav();
+ }
+ }
+ if (newRepliesInThread > 0 && !inThread) {
+ thread.getElementsByClassName("threadlinks")[0].innerHTML += "<span onClick='localStorage.setItem(\"" + boardName + "_" + threadId + "\" , " + lastReplyN + "); this.hidden = true;' style='font-weight: bold; background: #81a2be; padding: 5px; border-radius: 5px; float: right; margin-bottom: 10px;'>Marcar como leido</span>";
+ }
+
+ }
+ if (newRepliesCounter > 0 && !inThread) {
+ var banner = document.createElement("span");
+ banner.onclick = function() { this.hidden = true; };
+ banner.setAttribute("style", "font-weight: bold; background: #8c9440; padding: 8px; border-radius: 30px; float: right; position: fixed; bottom: 10px; right: 10px");
+ banner.textContent = "Nuevas respuestas: " + newRepliesCounter;
+ document.body.appendChild(banner);
+ }
+
+ if (inThread) {
+ localStorage.setItem(boardName + "_" + threadId, lastReplyN);
+ }
+
+}
+
+function on_checked(e) {
+ localStorage.setItem(e.target.id, e.target.checked);
+}
+function createCheckbox(name, label, def) {
+ var lbl = document.createElement("label");
+ var chk = document.createElement("input");
+ chk.type = "checkbox";
+ chk.id = name;
+ chk.onchange = on_checked;
+ lbl.appendChild(chk);
+ lbl.insertAdjacentHTML("beforeend", " "+label+" ");
+
+ var checked = localStorage.getItem(name);
+ if(checked !== null) {
+ chk.checked = (checked == "true");
+ } else {
+ chk.checked = def;
+ }
+
+ return lbl;
+}
+function createOption(name, label) {
+ var opt = document.createElement("option");
+ opt.value = name;
+ opt.text = label;
+ return opt;
+}
+function createButton(label, func) {
+ var btn = document.createElement("button");
+ btn.type = "button";
+ btn.textContent = label;
+ btn.onclick = func;
+ return btn;
+}
+function createTh(label, w) {
+ var th = document.createElement("th");
+ th.textContent = label;
+ th.width = w;
+ return th;
+}
+function loadFilters() {
+ var filters = JSON.parse(localStorage.getItem("shobon_filters"));
+
+ if(filters) {
+ are_filters = true;
+ hide_word = new Set(filters.word);
+ hide_name = new Set(filters.name);
+ hide_id = new Set(filters.id);
+ }
+}
+function saveFilters() {
+ var filters = {
+ "word": Array.from(hide_word),
+ "name": Array.from(hide_name),
+ "id": Array.from(hide_id)
+ };
+ localStorage.setItem("shobon_filters", JSON.stringify(filters));
+}
+function deleteFilter(e) {
+ var tr = this.parentElement.parentElement;
+ var name = tr.dataset.name;
+ var type = tr.dataset.type;
+ switch(type) {
+ case "word":
+ hide_word.delete(name);
+ break;
+ case "name":
+ hide_name.delete(name);
+ break;
+ case "id":
+ hide_id.delete(name);
+ break;
+ }
+ saveFilters();
+ tr.remove();
+}
+function addFilter(e) {
+ var name = document.getElementById("txt_filter").value;
+ var type = document.getElementById("lst_type").value;
+ if(!name) {
+ return;
+ }
+
+ switch(type) {
+ case "word":
+ hide_word.add(name);
+ break;
+ case "name":
+ hide_name.add(name);
+ break;
+ case "id":
+ hide_id.add(name);
+ break;
+ }
+ addToFilterTable(name, type);
+ saveFilters();
+ document.getElementById("txt_filter").value = "";
+}
+
+function addToFilterTable(name, type) {
+ var dict = {"word": "Palabra", "name": "Nombre/Tripcode", "id": "ID"};
+ var table = document.getElementById("tbl_filters");
+
+ var td_type = document.createElement("td");
+ td_type.textContent = dict[type];
+ var td_name = document.createElement("td");
+ td_name.textContent = name;
+ var td_btn = document.createElement("td");
+ td_btn.appendChild(createButton("X", deleteFilter));
+
+ var tr = document.createElement("tr");
+ tr.dataset.type = type;
+ tr.dataset.name = name;
+ tr.appendChild(td_type);
+ tr.appendChild(td_name);
+ tr.appendChild(td_btn);
+
+ table.appendChild(tr);
+}
+function shobonSettings(e) {
+ e.preventDefault();
+
+ var titlebox = document.getElementById("titlebox");
+
+ var box = document.getElementById("settings");
+ if(box) {
+ box.hidden = !box.hidden;
+ } else {
+ box = document.createElement("div");
+ box.id = "settings";
+ box.className = "innerbox";
+ box.style.textAlign = "center";
+
+ var p = document.createElement("div");
+ p.appendChild(createCheckbox("shobon_on", "<b>Activar extensión</b>", true));
+ p.appendChild(createCheckbox("shobon_navbar", "Fijar barra superior", false));
+ p.appendChild(createCheckbox("shobon_ids", "Colorear IDs", true));
+ p.appendChild(createCheckbox("shobon_newposts", "Destacar mensajes nuevos", false));
+ p.appendChild(createCheckbox("shobon_country", "Reemplazar códigos de país por nombres", false));
+ p.appendChild(createCheckbox("shobon_time", "Convertir fechas a hora local", true));
+ p.appendChild(createCheckbox("shobon_backlink", "Mostrar quién ha citado un post", true));
+ p.appendChild(createCheckbox("shobon_preview", "Previsualizar citas", true));
+ p.appendChild(createCheckbox("shobon_usefilters", "Activar filtros", false));
+ /*var a = document.createElement("a");
+ a.href = "#";
+ a.innerText = "[Editar filtros]";
+ a.addEventListener("click", function() {
+ var x = document.getElementById("filters");
+ x.hidden = !x.hidden;
+ });
+ p.appendChild(a);*/
+ box.appendChild(p);
+
+ var title2 = document.createElement("h6");
+ title2.textContent = "Filtros";
+ title2.style.fontSize = "18px";
+ title2.style.margin = "0.5em 0";
+ box.appendChild(title2);
+
+ box.appendChild(document.createTextNode("Filtrar mensajes por: "));
+
+ var lst_type = document.createElement("select");
+ lst_type.id = "lst_type";
+ lst_type.appendChild(createOption("word", "Palabra"));
+ lst_type.appendChild(createOption("name", "Nombre/Tripcode"));
+ lst_type.appendChild(createOption("id", "ID"));
+ box.appendChild(lst_type);
+
+ var txt_filter = document.createElement("input");
+ txt_filter.id = "txt_filter";
+ txt_filter.type = "text";
+ box.appendChild(txt_filter);
+
+ box.appendChild(createButton("Agregar", addFilter));
+
+ var tbl_filters = document.createElement("table");
+ tbl_filters.id = "tbl_filters";
+ tbl_filters.border = "1";
+ tbl_filters.style.margin = "0 auto";
+ var row = document.createElement("tr");
+ row.appendChild(createTh("Tipo", 150));
+ row.appendChild(createTh("Filtro", 300));
+ row.appendChild(createTh("", 75));
+ tbl_filters.appendChild(row);
+ box.appendChild(tbl_filters);
+
+ var msg = document.createElement("a");
+ msg.style.display = "block";
+ msg.href = "#";
+ msg.textContent = "Actualizar página para ver cambios";
+ msg.style.marginTop = "1em";
+ msg.addEventListener("click", function() { location.reload(); });
+ box.appendChild(msg);
+
+ titlebox.appendChild(box);
+
+ var i;
+ hide_word.forEach(v => {
+ addToFilterTable(v, "word")
+ });
+ hide_name.forEach(v => {
+ addToFilterTable(v, "name")
+ });
+ hide_id.forEach(v => {
+ addToFilterTable(v, "id")
+ });
+ }
+}
+
+function replaceCountryName(reply) {
+ var country = JSON.parse('{"AF":"Afghanistan","AX":"A£land Islands","AL":"Albania","DZ":"Algeria","AS":"American Samoa","AD":"Andorra","AO":"Angola","AI":"Anguilla","AQ":"Antarctica","AG":"Antigua and Barbuda","AR":"Argentina","AM":"Armenia","AW":"Aruba","AU":"Australia","AT":"Austria","AZ":"Azerbaijan","BS":"Bahamas","BH":"Bahrain","BD":"Bangladesh","BB":"Barbados","BY":"Belarus","BE":"Belgium","BZ":"Belize","BJ":"Benin","BM":"Bermuda","BT":"Bhutan","BO":"Bolivia (Plurinational State of)","BQ":"Bonaire, Sint Eustatius and Saba","BA":"Bosnia and Herzegovina","BW":"Botswana","BV":"Bouvet Island","BR":"Brazil","IO":"British Indian Ocean Territory","BN":"Brunei Darussalam","BG":"Bulgaria","BF":"Burkina Faso","BI":"Burundi","KH":"Cambodia","CM":"Cameroon","CA":"Canada","CV":"Cabo Verde","KY":"Cayman Islands","CF":"Central African Republic","TD":"Chad","CL":"Chile","CN":"China","CX":"Christmas Island","CC":"Cocos (Keeling) Islands","CO":"Colombia","KM":"Comoros","CG":"Congo","CD":"Congo (Democratic Republic of the)","CK":"Cook Islands","CR":"Costa Rica","CI":"CAŒte d\'Ivoire","HR":"Croatia","CU":"Cuba","CW":"CuraAXao","CY":"Cyprus","CZ":"Czech Republic","DK":"Denmark","DJ":"Djibouti","DM":"Dominica","DO":"Dominican Republic","EC":"Ecuador","EG":"Egypt","SV":"El Salvador","GQ":"Equatorial Guinea","ER":"Eritrea","EE":"Estonia","ET":"Ethiopia","FK":"Falkland Islands (Malvinas)","FO":"Faroe Islands","FJ":"Fiji","FI":"Finland","FR":"France","GF":"French Guiana","PF":"French Polynesia","TF":"French Southern Territories","GA":"Gabon","GM":"Gambia","GE":"Georgia","DE":"Germany","GH":"Ghana","GI":"Gibraltar","GR":"Greece","GL":"Greenland","GD":"Grenada","GP":"Guadeloupe","GU":"Guam","GT":"Guatemala","GG":"Guernsey","GN":"Guinea","GW":"Guinea-Bissau","GY":"Guyana","HT":"Haiti","HM":"Heard Island and McDonald Islands","VA":"Holy See","HN":"Honduras","HK":"Hong Kong","HU":"Hungary","IS":"Iceland","IN":"India","ID":"Indonesia","IR":"Iran (Islamic Republic of)","IQ":"Iraq","IE":"Ireland","IM":"Isle of Man","IL":"Israel","IT":"Italy","JM":"Jamaica","JP":"Japan","JE":"Jersey","JO":"Jordan","KZ":"Kazakhstan","KE":"Kenya","KI":"Kiribati","KP":"Korea (Democratic People\'s Republic of)","KR":"Korea (Republic of)","KW":"Kuwait","KG":"Kyrgyzstan","LA":"Lao People\'s Democratic Republic","LV":"Latvia","LB":"Lebanon","LS":"Lesotho","LR":"Liberia","LY":"Libya","LI":"Liechtenstein","LT":"Lithuania","LU":"Luxembourg","MO":"Macao","MK":"Macedonia (the former Yugoslav Republic of)","MG":"Madagascar","MW":"Malawi","MY":"Malaysia","MV":"Maldives","ML":"Mali","MT":"Malta","MH":"Marshall Islands","MQ":"Martinique","MR":"Mauritania","MU":"Mauritius","YT":"Mayotte","MX":"Mexico","FM":"Micronesia (Federated States of)","MD":"Moldova (Republic of)","MC":"Monaco","MN":"Mongolia","ME":"Montenegro","MS":"Montserrat","MA":"Morocco","MZ":"Mozambique","MM":"Myanmar","NA":"Namibia","NR":"Nauru","NP":"Nepal","NL":"Netherlands","NC":"New Caledonia","NZ":"New Zealand","NI":"Nicaragua","NE":"Niger","NG":"Nigeria","NU":"Niue","NF":"Norfolk Island","MP":"Northern Mariana Islands","NO":"Norway","OM":"Oman","PK":"Pakistan","PW":"Palau","PS":"Palestine, State of","PA":"Panama","PG":"Papua New Guinea","PY":"Paraguay","PE":"Peru","PH":"Philippines","PN":"Pitcairn","PL":"Poland","PT":"Portugal","PR":"Puerto Rico","QA":"Qatar","RE":"RAcunion","RO":"Romania","RU":"Russian Federation","RW":"Rwanda","BL":"Saint BarthAclemy","SH":"Saint Helena, Ascension and Tristan da Cunha","KN":"Saint Kitts and Nevis","LC":"Saint Lucia","MF":"Saint Martin (French part)","PM":"Saint Pierre and Miquelon","VC":"Saint Vincent and the Grenadines","WS":"Samoa","SM":"San Marino","ST":"Sao Tome and Principe","SA":"Saudi Arabia","SN":"Senegal","RS":"Serbia","SC":"Seychelles","SL":"Sierra Leone","SG":"Singapore","SX":"Sint Maarten (Dutch part)","SK":"Slovakia","SI":"Slovenia","SB":"Solomon Islands","SO":"Somalia","ZA":"South Africa","GS":"South Georgia and the South Sandwich Islands","SS":"South Sudan","ES":"Spain","LK":"Sri Lanka","SD":"Sudan","SR":"Suriname","SJ":"Svalbard and Jan Mayen","SZ":"Swaziland","SE":"Sweden","CH":"Switzerland","SY":"Syrian Arab Republic","TW":"Taiwan, Province of China","TJ":"Tajikistan","TZ":"Tanzania, United Republic of","TH":"Thailand","TL":"Timor-Leste","TG":"Togo","TK":"Tokelau","TO":"Tonga","TT":"Trinidad and Tobago","TN":"Tunisia","TR":"Turkey","TM":"Turkmenistan","TC":"Turks and Caicos Islands","TV":"Tuvalu","UG":"Uganda","UA":"Ukraine","AE":"United Arab Emirates","GB":"United Kingdom of Great Britain and Northern Ireland","US":"United States of America","UM":"United States Minor Outlying Islands","UY":"Uruguay","UZ":"Uzbekistan","VU":"Vanuatu","VE":"Venezuela (Bolivarian Republic of)","VN":"Viet Nam","VG":"Virgin Islands (British)","VI":"Virgin Islands (U.S.)","WF":"Wallis and Futuna","EH":"Western Sahara","YE":"Yemen","ZM":"Zambia","ZW":"Zimbabwe"}');
+ var countryEM = reply.getElementsByTagName("em")[0];
+ var newCountryStr = country[countryEM.innerText.substr(1, 2)];
+ countryEM.innerText = "(" + newCountryStr + ")";
+}
+
+function checkBlackList(reply) {
+ var i;
+
+ // Check words
+ var low = reply.children[1].innerText.toLowerCase();
+ hide_word.forEach(v => {
+ console.log(v);
+ if (low.includes(v.toLowerCase())) {
+ hidepost(reply);
+ }
+ });
+
+ // Check name/trip
+ var msg_name = reply.firstElementChild.getElementsByClassName("name")[0].textContent.toLowerCase();
+ hide_name.forEach(v => {
+ if (msg_name.includes(hide_name[i])) {
+ hidepost(reply);
+ }
+ });
+
+ // Check ID
+ var date_div = reply.firstElementChild.getElementsByClassName("date")[0].textContent;
+ var id_index = date_div.indexOf("ID:");
+ if(id_index != -1) {
+ var id = date_div.substr(id_index+3);
+
+ hide_id.forEach(v => {
+ if(id.includes(v)) {
+ hidepost(reply);
+ }
+ });
+ }
+}
+
+function paintIds(reply) {
+ var dateId = reply.getElementsByClassName("date")[0];
+ if (dateId.innerText.includes("ID:")) {
+ var postDate = dateId.innerText.split("ID:")[0];
+ var userId = dateId.innerText.split("ID:")[1];
+ var idColor = toHex(userId).substring(0, 6);
+ var reverseColor = invertColor(idColor);
+ var lastChar = userId.substring(userId.length-1, userId.length);
+ dateId.innerHTML = postDate + "<span style='background-color:#" + idColor + "; color:#" + reverseColor + "; padding:0 3px; border-radius:5px; font-size:0.9em; vertical-align:top;'>ID:" + userId + "</span>";
+ }
+}
+
+function toHex(str) {
+ var hex = '';
+ for (var i = 0; i < str.length; i++) {
+ hex += '' + (str.charCodeAt(i) + 125).toString(16);
+ }
+ return hex;
+}
+
+function invertColor(hex) {
+ if (hex.indexOf('#') === 0) {
+ hex = hex.slice(1);
+ }
+ // convert 3-digit hex to 6-digits.
+ if (hex.length === 3) {
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
+ }
+ if (hex.length !== 6) {
+ throw new Error('Invalid HEX color.');
+ }
+ // invert color components
+ var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16),
+ g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16),
+ b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16);
+ // pad each with zeros and return
+ return padZero(r) + padZero(g) + padZero(b);
+}
+
+function padZero(str, len) {
+ len = len || 2;
+ var zeros = new Array(len).join('0');
+ return (zeros + str).slice(-len);
+}
+
+function fixedNav() {
+ if (document.body.className === "mainpage" || document.body.className === "threads") {
+ document.getElementById("main_nav").style.position = "fixed";
+ document.getElementById("main_nav").style.top = "0";
+ document.body.style.marginTop = "2em";
+ }
+}
+
+document.addEventListener('DOMContentLoaded', shobon, false); \ No newline at end of file
diff --git a/static/js/tegaki/tegaki.css b/static/js/tegaki/tegaki.css
new file mode 100644
index 0000000..d2e3591
--- /dev/null
+++ b/static/js/tegaki/tegaki.css
@@ -0,0 +1,187 @@
+@font-face {
+ font-family: 'tegaki';
+ src: url('data:application/octet-stream;base64,d09GRgABAAAAAAyIAA4AAAAAFVAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEQAAABWPeFIsGNtYXAAAAGIAAAAOgAAAUrQFxm3Y3Z0IAAAAcQAAAAKAAAACgAAAABmcGdtAAAB0AAABZQAAAtwiJCQWWdhc3AAAAdkAAAACAAAAAgAAAAQZ2x5ZgAAB2wAAAI+AAAC7u/G5z9oZWFkAAAJrAAAADYAAAA2BIBHAWhoZWEAAAnkAAAAHgAAACQHlwNRaG10eAAACgQAAAAWAAAAIBsOAABsb2NhAAAKHAAAABIAAAASA2cCrm1heHAAAAowAAAAIAAAACAAmwu2bmFtZQAAClAAAAF+AAACte3MYkJwb3N0AAAL0AAAAFAAAABnZ1gGo3ByZXAAAAwgAAAAZQAAAHvdawOFeJxjYGROYpzAwMrAwVTFtIeBgaEHQjM+YDBkZGJgYGJgZWbACgLSXFMYHF4wvGBjDvqfxRDFzM3gDxRmBMkBANw6Cw94nGNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZgYGF6w/f8PUvCCAURLMELVAwEjG8OIBwBqdQa0AAAAAAAAAAAAAAAAAAB4nK1WaXMTRxCd1WHLNj6CDxI2gVnGcox2VpjLCBDG7EoW4BzylexCjl1Ldu6LT/wG/ZpekVSRb/y0vB4d2GAnVVQoSv2m9+1M9+ueXpPQksReWI+k3HwpprY2aWTnSUg3bFqO4kPZ2QspU0z+LoiCaLXUvu04JCISgap1hSWC2PfI0iTjQ48yWrYlvWpSbulJd9kaD+qt+vbT0FGO3QklNZuhQ+uRLanCqBJFMu2RkjYtw9VfSVrh5yvMfNUMJYLoJJLGm2EMj+Rn44xWGa3GdhxFkU2WG0WKRDM8iCKPslpin1wxQUD5oBlSXvk0onyEH5EVe5TTCnHJdprf9yU/6R3OvyTieouyJQf+QHZkB3unK/ki0toK46adbEehivB0fSfEI5uT6p/sUV7TaOB2RaYnzQiWyleQWPkJZfYPyWrhfMqXPBrVkoOcCFovc2Jf8g60HkdMiWsmyILujk6IoO6XnKHYY/q4+OO9XSwXIQTIOJb1jkq4EEYpYbOaJG0EOYiSskWV1HpHTJzyOi3iLWG/Tu3oS2e0Sag7MZ6th46tnKjkeDSp00ymTu2k5tGUBlFKOhM85tcBlB/RJK+2sZrEyqNpbDNjJJFQoIVzaSqIZSeWNAXRPJrRm7thmmvXokWaPFDPPXpPb26Fmzs9p+3AP2v8Z3UqpoO9MJ2eDshKfJp2uUnRun56hn8m8UPWAiqRLTbDlMVDtn4H5eVjS47CawNs957zK+h99kTIpIH4G/AeL9UpBUyFmFVQC9201rUsy9RqVotUZOq7IU0rX9ZpAk05Dn1jX8Y4/q+ZGUtMCd/vxOnZEZeeufYlyDSH3GZdj+Z1arFdgM5sz+k0y/Z9nebYfqDTPNvzOh1ha+t0lO2HOi2w/UinY2wvaEGT7jsEchGBXMAGEoGwdRAI20sIhK1CIGwXEQjbIgJhu4RA2H6MQNguIxC2l7Wsmn4qaRw7E8sARYgDoznuyGVuKldTyaUSrotGpzbkKXKrpKJ4Vv0rA/3ikTesgbVAukTW/IpJrnxUleOPrmh508S5Ao5Vf3tzXJ8TD2W/WPhT8L/amqqkV6x5ZHIVeSPQk+NE1yYVj67p8rmqR9f/i4oOa4F+A6UQC0VZlg2+mZDwUafTUA1c5RAzGzMP1/W6Zc3P4fybGCEL6H78NxQaC9yDTllJWe1gr9XXj2W5twflsCdYkmK+zOtb4YuMzEr7RWYpez7yecAVMCqVYasNXK3gzXsS85DpTfJMELcVZYOkjceZILGBYx4wb76TICRMXbWB2imcsIG8YMwp2O+EQ1RvlOVwe6F9Ho2Uf2tX7MgZFU0Q+G32Rtjrs1DyW6yBhCe/1NdAVSFNxbipgEsj5YZq8GFcrdtGMk6gr6jYDcuyig8fR9x3So5lIPlIEatHRz+tvUKd1Ln9yihu3zv9CIJBaWL+9r6Z4qCUd7WSZVZtA1O3GpVT15rDxasO3c2j7nvH2Sdy1jTddE/c9L6mVbeDg7lZEO3bHJSlTC6o68MOG6jLzaXQ6mVckt52DzAsMKDfoRUb/1f3cfg8V6oKo+NIvZ2oH6PPYgzyDzh/R/UF6OcxTLmGlOd7lxOfbtzD2TJdxV2sn+LfwKy15mbpGnBD0w2Yh6xaHbrKDXynBjo90tyO9BDwse4K8QBgE8Bi8InuWsbzKYDxfMYcH+Bz5jBoMofBFnMYbDNnDWCHOQx2mcNgjzkMvmDOOsCXzGEQModBxBwGT5gTADxlDoOvmMPga+Yw+IY59wG+ZQ6DmDkMEuYw2Nd0ayhzixd0F6htUBXowPQTFvewONRUGbK/44Vhf28Qs38wiKk/aro9pP7EC0P92SCm/mIQU3/VdGdI/Y0Xhvq7QUz9wyCmPtMvxnKZwV9GvkuFA8ouNp/z98T7B8IaQLYAAQAB//8AD3icZZI/bNNAFMbvnYOd3KW1kzhnqUQmdVo7FQWi/LGlMKDSUglRZesAylSKVCkMiB2UShUqE1LGSERCSlmYIFIr5q4MDFUpTN1IB8Rahjq8c9oy4OHzu3dPv+/u3iNAyOg3PaCvyAxRByIGN67Pmjqozi3QpLjVO+BJ8cvXIJAicNsS9EBfMeaNfh9lxZB/499a1/t9/ZmQwc6O/n+hflMWEOn9R0krnBTQeyqB3pA1Va+AohUcN6iheLWqH1RQbkNZWNlKWSjpvBjmRUvkYWjZgAvbamEwxMSezJ4IzGZPLrOynOAHUpQ0/CI6+iWVC7/pc5fpMfvsUUSl7y94Y1CeKNF5h/QFSRGHVAjbK3lXTZ0qyHE9gSjHrVUDVcNiH6qu5qhZ0wYf2ZWyf8XU1Fh+Bh8z8OchZgnl3Wrb6XztOO3VB8cQOw4/G3x53RDGUokb8J03wtPwR3ja4LwBcXAh3uBQ31qoL250OhuL9YWt59vbcB9L1+8lJ2malZaML5nMZre7mXHNdpf2XprRnUc/lV06R0y8M6N45wR214NxT60EjHuqfAjXmM3CNc6b3GZQhCLPJZsc3oSPOYe3mGtyHh5hGgty52+5S5cjri65szgwXgLGUxNIeMSVuPAoPIpwHHo8J6XVZAzmwm+MRXY9Jq1zeN7R2egjvUv3yRRyOUFuBvtipbDx47F0AxyFVEFGfhpeawxaOJKfuMMGkwlmtQZx9aHG6D6Lh3YxczgxcZgSJjxRn2riL3t/mWkAAAABAAAAAQAAO8vwqV8PPPUACwPoAAAAANC+FsgAAAAA0L3smP/9/7ED6AMLAAAACAACAAAAAAAAeJxjYGRgYA76n8UQxfyCgeH/NyAJFEEBHACQkgXuAAB4nGN+wcDALIiEXyAwkzUDAwBBEgQmAAAAAAAAAD4AdgCWAPABHAFIAXcAAAABAAAACAA0AAMAAAAAAAIAAAAQAHMAAAAcC3AAAAAAeJx1kM1Kw0AUhc/U/mArLiy4HjeiiOkPurBuxELrSsFFQVzI2E6T1DRTJlOhr+A7+BC+kM/iSTJIEcwwk++ee+7NnQA4wDcEyueSu2SBOqOSK2jg2vMO9VvPVfKd5xpauPdc53ry3MQZXjy30MYHO4jqLqMFPj0L7ImG5wr2RdvzDvUjz1XyuecaDsWV5zr1B89NTMSz5xaOxdfQrDY2DiMnT4anst/tXcjXjTSU4lQlUq1dZGwmb+TcpE4niQmmZul0qN7iRx2uE2XLoDwn2maxSWUv6JbCWKfaKqdnedfsPew7N5dza5Zy5PvJlTULPXVB5Nxq0OlsfwdDGKywgUWMEBEcJE6onvLdRxc9XJBe6ZB0lq4YKRQSKgprVkRFJmN8wz1nlFLVdCTkAFOey0IJWfHG+seC18wrVm5ntnlCzvvGRUfJWQJOtO0Yk9PCpQp99jtrhne6+lQdJ8qnssUUEqM/80neP88tqEypB8VfcFQH6HD9c58fnU58DwAAeJxjYGKAAC4G7ICDgYGRiZGZkYWRlZGNkZ2Rgy05MS85NYelIKe0mDU3M6+0mDm1MpUzJb88Tze/IDWPvbQATHPlpJal5uiCxBkYAP+wElx4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGVidNjIwaEFoDhR6JwMDAycyi5nBZaMKY0dgxAaHjoiNzCkuG9VAvF0cDQyMLA4dySERICWRQLCRgUdrB+P/1g0svRuZGFwAB9MiuAAAAA==') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.tegaki-icon:before {
+ font-size: 10px;
+ width: 10px;
+ font-family: 'tegaki';
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+ display: inline-block;
+ text-align: center;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1em;
+}
+
+.tegaki-cancel:before { content: '\e800'; } /* 'î €' */
+.tegaki-plus:before { content: '\e801'; } /* 'î ' */
+.tegaki-minus:before { content: '\e802'; } /* 'î ‚' */
+.tegaki-eye:before { content: '\e803'; } /* 'î ƒ' */
+.tegaki-down-open:before { content: '\e804'; } /* 'î „' */
+.tegaki-up-open:before { content: '\e805'; } /* 'î …' */
+.tegaki-level-down:before { content: '\e806'; } /* 'î †' */
+
+#tegaki {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: #a3b1bf;
+ color: #000;
+ font-family: arial, sans-serif;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ overflow: auto;
+ z-index: 9999;
+ image-rendering: optimizeSpeed;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ image-rendering: -moz-crisp-edges;
+}
+
+#tegaki-debug {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+#tegaki-debug canvas {
+ width: 75px;
+ height: 75px;
+ display: block;
+ border: 1px solid black;
+}
+
+.tegaki-backdrop {
+ overflow: hidden;
+}
+
+.tegaki-hidden {
+ display: none !important;
+}
+
+.tegaki-strike {
+ text-decoration: line-through;
+}
+
+#tegaki-cnt {
+ left: 50%;
+ top: 50%;
+ position: absolute;
+}
+
+#tegaki-cnt.tegaki-overflow-x {
+ left: 10px;
+ margin-left: 0 !important;
+}
+
+#tegaki-cnt.tegaki-overflow-y {
+ top: 10px;
+ margin-top: 0 !important;
+}
+
+.tegaki-tb-btn {
+ margin-left: 10px;
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.tegaki-tb-btn:hover {
+ color: #007FFF;
+}
+
+.tegaki-tb-btn:focus {
+ color: #007FFF;
+ outline: none;
+}
+
+#tegaki-menu-bar {
+ font-size: 12px;
+ white-space: nowrap;
+ position: absolute;
+ right: 0;
+}
+
+#tegaki-canvas {
+ -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
+ background: #FFFAFA;
+}
+
+#tegaki-layers {
+ display: inline-block;
+ font-size: 0;
+}
+
+#tegaki-finish-btn {
+ font-weight: bold;
+}
+
+.tegaki-ctrlgrp {
+ margin-bottom: 5px;
+}
+
+.tegaki-label {
+ font-size: 10px;
+}
+
+.tegaki-label:after {
+ content: ' ' attr(data-value);
+}
+
+#tegaki-ghost-layer,
+.tegaki-layer {
+ position: absolute;
+ left: 0;
+}
+
+#tegaki-ctrl {
+ position: absolute;
+ display: inline-block;
+ width: 80px;
+ padding-left: 5px;
+ font-size: 14px;
+}
+
+#tegaki-color {
+ padding: 0;
+ border: 0;
+ display: block;
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+}
+
+#tegaki-layer-grp span {
+ font-size: 12px;
+ margin-right: 3px;
+ cursor: pointer;
+}
+
+#tegaki-layer-grp span:hover {
+ color: #007FFF;
+}
+
+#tegaki-color::-moz-focus-inner {
+ border: none;
+ padding: 0;
+}
+
+#tegaki-alpha,
+#tegaki-size {
+ width: 90%;
+ margin: auto;
+}
+
+#tegaki-ctrl select {
+ font-size: 11px;
+ width: 100%;
+}
diff --git a/static/js/tegaki/tegaki.js b/static/js/tegaki/tegaki.js
new file mode 100644
index 0000000..3d38d00
--- /dev/null
+++ b/static/js/tegaki/tegaki.js
@@ -0,0 +1,1947 @@
+var TegakiBrush = {
+ brushFn: function(x, y) {
+ var i, ctx, dest, data, len, kernel;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ ctx = Tegaki.ghostCtx;
+ dest = ctx.getImageData(x, y, this.brushSize, this.brushSize);
+ data = dest.data;
+ kernel = this.kernel;
+ len = kernel.length;
+
+ i = 0;
+ while (i < len) {
+ data[i] = this.rgb[0]; ++i;
+ data[i] = this.rgb[1]; ++i;
+ data[i] = this.rgb[2]; ++i;
+ data[i] += kernel[i] * (1.0 - data[i] / 255); ++i;
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ commit: function() {
+ Tegaki.activeCtx.drawImage(Tegaki.ghostCanvas, 0, 0);
+ Tegaki.ghostCtx.clearRect(0, 0,
+ Tegaki.ghostCanvas.width, Tegaki.ghostCanvas.height
+ );
+ },
+
+ draw: function(posX, posY, pt) {
+ var offset, mx, my, fromX, fromY, dx, dy, err, derr, step, stepAcc;
+
+ offset = this.center;
+ step = this.stepSize;
+ stepAcc = this.stepAcc;
+
+ if (pt === true) {
+ this.stepAcc = 0;
+ this.posX = posX;
+ this.posY = posY;
+ this.brushFn(posX - offset, posY - offset);
+ return;
+ }
+
+ fromX = this.posX;
+ fromY = this.posY;
+
+ if (fromX < posX) { dx = posX - fromX; mx = 1; }
+ else { dx = fromX - posX; mx = -1; }
+ if (fromY < posY) { dy = posY - fromY; my = 1; }
+ else { dy = fromY - posY; my = -1; }
+
+ err = (dx > dy ? dx : -dy) / 2;
+
+ dx = -dx;
+
+ while (true) {
+ ++stepAcc;
+ if (stepAcc > step) {
+ this.brushFn(fromX - offset, fromY - offset);
+ stepAcc = 0;
+ }
+ if (fromX === posX && fromY === posY) {
+ break;
+ }
+ derr = err;
+ if (derr > dx) { err -= dy; fromX += mx; }
+ if (derr < dy) { err -= dx; fromY += my; }
+ }
+
+ this.stepAcc = stepAcc;
+ this.posX = posX;
+ this.posY = posY;
+ },
+
+ generateBrush: function() {
+ var i, size, r, brush, ctx, dest, data, len, sqd, sqlen, hs, col, row,
+ ecol, erow, a;
+
+ size = this.size * 2;
+ r = size / 2;
+
+ brush = T$.el('canvas');
+ brush.width = brush.height = size;
+ ctx = brush.getContext('2d');
+ dest = ctx.getImageData(0, 0, size, size);
+ data = dest.data;
+ len = size * size * 4;
+ sqlen = Math.sqrt(r * r);
+ hs = Math.round(r);
+ col = row = -hs;
+
+ i = 0;
+ while (i < len) {
+ if (col >= hs) {
+ col = -hs;
+ ++row;
+ continue;
+ }
+
+ ecol = col;
+ erow = row;
+
+ if (ecol < 0) { ecol = -ecol; }
+ if (erow < 0) { erow = -erow; }
+
+ sqd = Math.sqrt(ecol * ecol + erow * erow);
+
+ if (sqd > sqlen) {
+ a = 0;
+ }
+ else {
+ a = sqd / sqlen;
+ a = (Math.exp(1 - 1 / a) / a);
+ a = 255 - ((0 | (a * 100 + 0.5)) / 100) * 255;
+ }
+
+ if (this.alphaDamp) {
+ a *= this.alpha * this.alphaDamp;
+ }
+ else {
+ a *= this.alpha;
+ }
+
+ data[i + 3] = a;
+
+ i += 4;
+
+ ++col;
+ }
+
+ ctx.putImageData(dest, 0, 0);
+
+ this.center = r;
+ this.brushSize = size;
+ this.brush = brush;
+ this.kernel = data;
+ },
+
+ setSize: function(size, noBrush) {
+ this.size = size;
+ if (!noBrush) this.generateBrush();
+ this.stepSize = Math.floor(this.size * this.step);
+ },
+
+ setAlpha: function(alpha, noBrush) {
+ this.alpha = alpha;
+ if (!noBrush) this.generateBrush();
+ },
+
+ setColor: function(color, noBrush) {
+ this.rgb = Tegaki.hexToRgb(color);
+ if (!noBrush) this.generateBrush();
+ },
+
+ set: function() {
+ this.setAlpha(this.alpha, true);
+ this.setSize(this.size, true);
+ this.setColor(Tegaki.toolColor, true);
+ this.generateBrush();
+ }
+};
+
+var TegakiPen = {
+ init: function() {
+ this.size = 4;
+ this.alpha = 0.5;
+ this.step = 0.1;
+ this.stepAcc = 0;
+ },
+
+ draw: TegakiBrush.draw,
+
+ commit: TegakiBrush.commit,
+
+ brushFn: TegakiBrush.brushFn,
+
+ generateBrush: function() {
+ var size, r, brush, ctx;
+
+ size = this.size;
+ r = size / 2;
+
+ brush = T$.el('canvas');
+ brush.width = brush.height = size;
+ ctx = brush.getContext('2d');
+ ctx.globalAlpha = this.alpha;
+ ctx.beginPath();
+ ctx.arc(r, r, r, 0, Tegaki.TWOPI, false);
+ ctx.fillStyle = '#000000';
+ ctx.fill();
+ ctx.closePath();
+
+ this.center = r;
+ this.brushSize = size;
+ this.brush = brush;
+ this.kernel = ctx.getImageData(0, 0, this.brushSize, this.brushSize).data;
+ },
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiPipette = {
+ size: 1,
+ alpha: 1,
+ noCursor: true,
+
+ draw: function(posX, posY) {
+ var c, ctx;
+
+ if (true) {
+ ctx = Tegaki.flatten().getContext('2d');
+ }
+ else {
+ ctx = Tegaki.activeCtx;
+ }
+
+ c = Tegaki.getColorAt(ctx, posX, posY);
+
+ Tegaki.setToolColor(c);
+ Tegaki.updateUI('color');
+ }
+};
+
+var TegakiAirbrush = {
+ init: function() {
+ this.size = 32;
+ this.alpha = 0.5;
+ this.alphaDamp = 0.2;
+ this.step = 0.25;
+ this.stepAcc = 0;
+ },
+
+ draw: TegakiBrush.draw,
+
+ commit: TegakiBrush.commit,
+
+ brushFn: TegakiBrush.brushFn,
+
+ generateBrush: TegakiBrush.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiPencil = {
+ init: function() {
+ this.size = 1;
+ this.alpha = 1.0;
+ this.step = 0.25;
+ this.stepAcc = 0;
+ },
+
+ draw: TegakiBrush.draw,
+
+ commit: TegakiBrush.commit,
+
+ brushFn: function(x, y) {
+ var i, ctx, dest, data, len, kernel, a;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ ctx = Tegaki.ghostCtx;
+ dest = ctx.getImageData(x, y, this.brushSize, this.brushSize);
+ data = dest.data;
+ kernel = this.kernel;
+ len = kernel.length;
+
+ a = this.alpha * 255;
+
+ i = 0;
+ while (i < len) {
+ data[i] = this.rgb[0]; ++i;
+ data[i] = this.rgb[1]; ++i;
+ data[i] = this.rgb[2]; ++i;
+ if (kernel[i] > 0) {
+ data[i] = a;
+ }
+ ++i;
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ generateBrush: TegakiPen.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiEraser = {
+ init: function() {
+ this.size = 8;
+ this.alpha = 1.0;
+ this.step = 0.25;
+ this.stepAcc = 0;
+ },
+
+ draw: TegakiBrush.draw,
+
+ brushFn: function(x, y) {
+ var i, ctx, dest, data, len, kernel;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ ctx = Tegaki.activeCtx;
+ dest = ctx.getImageData(x, y, this.brushSize, this.brushSize);
+ data = dest.data;
+ kernel = this.kernel;
+ len = kernel.length;
+
+ for (i = 3; i < len; i += 4) {
+ if (kernel[i] > 0) {
+ data[i] = 0;
+ }
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ generateBrush: TegakiPen.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiDodge = {
+ init: function() {
+ this.size = 24;
+ this.alpha = 0.25;
+ this.alphaDamp = 0.05;
+ this.step = 0.25;
+ this.stepAcc = 0;
+ },
+
+ brushFn: function(x, y) {
+ var i, a, aa, ctx, dest, data, len, kernel;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ ctx = Tegaki.activeCtx;
+ dest = ctx.getImageData(x, y, this.brushSize, this.brushSize);
+ data = dest.data;
+ kernel = this.kernel;
+ len = kernel.length;
+
+ i = 0;
+ while (i < len) {
+ aa = kernel[i + 3] * 0.3;
+ a = 1 + kernel[i + 3] / 255;
+ data[i] = data[i] * a + aa; ++i;
+ data[i] = data[i] * a + aa; ++i;
+ data[i] = data[i] * a + aa; ++i;
+ ++i;
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ draw: TegakiBrush.draw,
+
+ generateBrush: TegakiBrush.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiBurn = {
+ init: TegakiDodge.init,
+
+ brushFn: function(x, y) {
+ var i, a, ctx, dest, data, len, kernel;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ ctx = Tegaki.activeCtx;
+ dest = ctx.getImageData(x, y, this.brushSize, this.brushSize);
+ data = dest.data;
+ kernel = this.kernel;
+ len = kernel.length;
+
+ i = 0;
+ while (i < len) {
+ a = 1 - kernel[i + 3] / 255;
+ data[i] = data[i] * a; ++i;
+ data[i] = data[i] * a; ++i;
+ data[i] = data[i] * a; ++i;
+ ++i;
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ draw: TegakiBrush.draw,
+
+ generateBrush: TegakiDodge.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiBlur = {
+ init: TegakiDodge.init,
+
+ brushFn: function(x, y) {
+ var i, j, ctx, src, size, srcData, dest, destData, lim, kernel,
+ sx, sy, r, g, b, a, aa, acc, kx, ky;
+
+ x = 0 | x;
+ y = 0 | y;
+
+ size = this.brushSize;
+ ctx = Tegaki.activeCtx;
+ src = ctx.getImageData(x, y, size, size);
+ srcData = src.data;
+ dest = ctx.createImageData(size, size);
+ destData = dest.data;
+ kernel = this.kernel;
+ lim = size - 1;
+
+ for (sx = 0; sx < size; ++sx) {
+ for (sy = 0; sy < size; ++sy) {
+ r = g = b = a = acc = 0;
+ i = (sy * size + sx) * 4;
+ if (kernel[(sy * size + sx) * 4 + 3] === 0
+ || sx === 0 || sy === 0 || sx === lim || sy === lim) {
+ destData[i] = srcData[i]; ++i;
+ destData[i] = srcData[i]; ++i;
+ destData[i] = srcData[i]; ++i;
+ destData[i] = srcData[i];
+ continue;
+ }
+ for (kx = -1; kx < 2; ++kx) {
+ for (ky = -1; ky < 2; ++ky) {
+ j = ((sy - ky) * size + (sx - kx)) * 4;
+ aa = srcData[j + 3];
+ acc += aa;
+ r += srcData[j] * aa; ++j;
+ g += srcData[j] * aa; ++j;
+ b += srcData[j] * aa; ++j;
+ a += srcData[j];
+ }
+ }
+ destData[i] = r / acc; ++i;
+ destData[i] = g / acc; ++i;
+ destData[i] = b / acc; ++i;
+ destData[i] = a / 9;
+ }
+ }
+
+ ctx.putImageData(dest, x, y);
+ },
+
+ draw: TegakiBrush.draw,
+
+ generateBrush: TegakiDodge.generateBrush,
+
+ setSize: TegakiBrush.setSize,
+
+ setAlpha: TegakiBrush.setAlpha,
+
+ setColor: TegakiBrush.setColor,
+
+ set: TegakiBrush.set
+};
+
+var TegakiHistory = {
+ maxSize: 10,
+
+ undoStack: [],
+ redoStack: [],
+
+ pendingAction: null,
+
+ clear: function() {
+ this.undoStack = [];
+ this.redoStack = [];
+ this.pendingAction = null;
+ },
+
+ push: function(action) {
+ this.undoStack.push(action);
+
+ if (this.undoStack.length > this.maxSize) {
+ this.undoStack.shift();
+ }
+
+ if (this.redoStack.length > 0) {
+ this.redoStack = [];
+ }
+ },
+
+ undo: function() {
+ var action;
+
+ if (!this.undoStack.length) {
+ return;
+ }
+
+ action = this.undoStack.pop();
+ action.undo();
+
+ this.redoStack.push(action);
+ },
+
+ redo: function() {
+ var action;
+
+ if (!this.redoStack.length) {
+ return;
+ }
+
+ action = this.redoStack.pop();
+ action.redo();
+
+ this.undoStack.push(action);
+ }
+};
+
+var TegakiHistoryActions = {
+ Draw: function(layerId) {
+ this.canvasBefore = null;
+ this.canvasAfter = null;
+ this.layerId = layerId;
+ },
+
+ DestroyLayers: function(indices, layers) {
+ this.indices = indices;
+ this.layers = layers;
+ this.canvasBefore = null;
+ this.canvasAfter = null;
+ this.layerId = null;
+ },
+
+ AddLayer: function(layerId) {
+ this.layerId = layerId;
+ },
+
+ MoveLayer: function(layerId, up) {
+ this.layerId = layerId;
+ this.up = up;
+ }
+};
+
+TegakiHistoryActions.Draw.prototype.addCanvasState = function(canvas, type) {
+ if (type) {
+ this.canvasAfter = T$.copyCanvas(canvas);
+ }
+ else {
+ this.canvasBefore = T$.copyCanvas(canvas);
+ }
+};
+
+TegakiHistoryActions.Draw.prototype.exec = function(type) {
+ var i, layer;
+
+ for (i in Tegaki.layers) {
+ layer = Tegaki.layers[i];
+
+ if (layer.id === this.layerId) {
+ layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
+ layer.ctx.drawImage(type ? this.canvasAfter: this.canvasBefore, 0, 0);
+ }
+ }
+};
+
+TegakiHistoryActions.Draw.prototype.undo = function() {
+ this.exec(0);
+};
+
+TegakiHistoryActions.Draw.prototype.redo = function() {
+ this.exec(1);
+};
+
+TegakiHistoryActions.DestroyLayers.prototype.undo = function() {
+ var i, ii, len, layers, idx, layer, frag;
+
+ layers = new Array(len);
+
+ for (i = 0; (idx = this.indices[i]) !== undefined; ++i) {
+ layers[idx] = this.layers[i];
+ }
+
+ i = ii = 0;
+ len = Tegaki.layers.length + this.layers.length;
+ frag = T$.frag();
+
+ while (i < len) {
+ if (!layers[i]) {
+ layer = layers[i] = Tegaki.layers[ii];
+ Tegaki.layersCnt.removeChild(layer.canvas);
+ ++ii;
+ }
+
+ if (this.layerId && layer.id === this.layerId) {
+ layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
+ layer.ctx.drawImage(this.canvasBefore, 0, 0);
+ }
+
+ frag.appendChild(layers[i].canvas);
+
+ ++i;
+ }
+
+ Tegaki.layersCnt.insertBefore(frag, Tegaki.canvas.nextElementSibling);
+
+ Tegaki.layers = layers;
+
+ Tegaki.setActiveLayer();
+
+ Tegaki.rebuildLayerCtrl();
+};
+
+TegakiHistoryActions.DestroyLayers.prototype.redo = function() {
+ var i, layer, ids = [];
+
+ for (i = 0; layer = this.layers[i]; ++i) {
+ ids.push(layer.id);
+ }
+
+ if (this.layerId) {
+ ids.push(this.layerId);
+ Tegaki.mergeLayers(ids);
+ }
+ else {
+ Tegaki.deleteLayers(ids);
+ }
+};
+
+TegakiHistoryActions.MoveLayer.prototype.undo = function() {
+ Tegaki.setActiveLayer(this.layerId);
+ Tegaki.moveLayer(this.layerId, !this.up);
+};
+
+TegakiHistoryActions.MoveLayer.prototype.redo = function() {
+ Tegaki.setActiveLayer(this.layerId);
+ Tegaki.moveLayer(this.layerId, this.up);
+};
+
+TegakiHistoryActions.AddLayer.prototype.undo = function() {
+ Tegaki.deleteLayers([this.layerId]);
+ Tegaki.layerIndex--;
+};
+
+TegakiHistoryActions.AddLayer.prototype.redo = function() {
+ Tegaki.addLayer();
+ Tegaki.setActiveLayer();
+};
+
+var T$ = {
+ docEl: document.documentElement,
+
+ id: function(id) {
+ return document.getElementById(id);
+ },
+
+ cls: function(klass, root) {
+ return (root || document).getElementsByClassName(klass);
+ },
+
+ on: function(o, e, h) {
+ o.addEventListener(e, h, false);
+ },
+
+ off: function(o, e, h) {
+ o.removeEventListener(e, h, false);
+ },
+
+ el: function(name) {
+ return document.createElement(name);
+ },
+
+ frag: function() {
+ return document.createDocumentFragment();
+ },
+
+ extend: function(destination, source) {
+ for (var key in source) {
+ destination[key] = source[key];
+ }
+ },
+
+ selectedOptions: function(el) {
+ var i, opt, sel;
+
+ if (el.selectedOptions) {
+ return el.selectedOptions;
+ }
+
+ sel = [];
+
+ for (i = 0; opt = el.options[i]; ++i) {
+ if (opt.selected) {
+ sel.push(opt);
+ }
+ }
+
+ return sel;
+ },
+
+ copyCanvas: function(source) {
+ var canvas = T$.el('canvas');
+ canvas.width = source.width;
+ canvas.height = source.height;
+ canvas.getContext('2d').drawImage(source, 0, 0);
+
+ return canvas;
+ }
+};
+
+var TegakiStrings = {
+ // Messages
+ badDimensions: 'Invalid dimensions.',
+ promptWidth: 'Canvas width in pixels',
+ promptHeight: 'Canvas height in pixels',
+ confirmDelLayers: 'Delete selected layers?',
+ errorMergeOneLayer: 'You need to select at least 2 layers.',
+ confirmMergeLayers: 'Merge selected layers?',
+ errorLoadImage: 'Could not load the image.',
+ noActiveLayer: 'No active layer.',
+ hiddenActiveLayer: 'The active layer is not visible.',
+ confirmCancel: 'Are you sure? Your work will be lost.',
+ confirmChangeCanvas: 'Changing the canvas will clear all layers and history.',
+
+ // UI
+ color: 'Color',
+ size: 'Size',
+ alpha: 'Opacity',
+ layers: 'Layers',
+ addLayer: 'Add layer',
+ delLayers: 'Delete layers',
+ mergeLayers: 'Merge layers',
+ showHideLayer: 'Toggle visibility',
+ moveLayerUp: 'Move up',
+ moveLayerDown: 'Move down',
+ tool: 'Tool',
+ changeCanvas: 'Change canvas',
+ blank: 'Blank',
+ newCanvas: 'New',
+ undo: 'Undo',
+ redo: 'Redo',
+ close: 'Close',
+ finish: 'Finish',
+
+ // Tools
+ pen: 'Pen',
+ pencil: 'Pencil',
+ airbrush: 'Airbrush',
+ pipette: 'Pipette',
+ dodge: 'Dodge',
+ burn: 'Burn',
+ blur: 'Blur',
+ eraser: 'Eraser'
+};
+
+var Tegaki = {
+ VERSION: '0.0.1',
+
+ bg: null,
+ cnt: null,
+ canvas: null,
+ ctx: null,
+ layers: [],
+ layersCnt: null,
+ ghostCanvas: null,
+ ghostCtx: null,
+ activeCtx: null,
+ activeLayer: null,
+ layerIndex: null,
+
+ isPainting: false,
+ isErasing: false,
+ isColorPicking: false,
+
+ offsetX: 0,
+ offsetY: 0,
+
+ TWOPI: 2 * Math.PI,
+
+ tools: {
+ pencil: TegakiPencil,
+ pen: TegakiPen,
+ airbrush: TegakiAirbrush,
+ pipette: TegakiPipette,
+ dodge: TegakiDodge,
+ burn: TegakiBurn,
+ blur: TegakiBlur,
+ eraser: TegakiEraser
+ },
+
+ tool: null,
+ toolColor: '#000000',
+
+ bgColor: '#ffffff',
+ maxSize: 32,
+ maxLayers: 25,
+ baseWidth: null,
+ baseHeight: null,
+
+ onDoneCb: null,
+ onCancelCb: null,
+
+ open: function(opts) {
+ var bg, cnt, el, el2, tool, lbl, btn, ctrl, canvas, grp, self = Tegaki;
+
+ if (self.bg) {
+ self.resume();
+ return;
+ }
+
+ if (opts.bgColor) {
+ self.bgColor = opts.bgColor;
+ }
+
+ self.onDoneCb = opts.onDone;
+ self.onCancelCb = opts.onCancel;
+
+ cnt = T$.el('div');
+ cnt.id = 'tegaki-cnt';
+
+ canvas = T$.el('canvas');
+ canvas.id = 'tegaki-canvas';
+ canvas.width = self.baseWidth = opts.width;
+ canvas.height = self.baseHeight = opts.height;
+
+ el = T$.el('div');
+ el.id = 'tegaki-layers';
+ el.appendChild(canvas);
+ self.layersCnt = el;
+
+ cnt.appendChild(el);
+
+ ctrl = T$.el('div');
+ ctrl.id = 'tegaki-ctrl';
+
+ // Colorpicker
+ grp = T$.el('div');
+ grp.className = 'tegaki-ctrlgrp';
+ el = T$.el('input');
+ el.id = 'tegaki-color';
+ el.value = self.toolColor;
+ try {
+ el.type = 'color';
+ } catch(e) {
+ el.type = 'text';
+ }
+ lbl = T$.el('div');
+ lbl.className = 'tegaki-label';
+ lbl.textContent = TegakiStrings.color;
+ grp.appendChild(lbl);
+ T$.on(el, 'change', self.onColorChange);
+ grp.appendChild(el);
+ ctrl.appendChild(grp);
+
+ // Size control
+ grp = T$.el('div');
+ grp.className = 'tegaki-ctrlgrp';
+ el = T$.el('input');
+ el.id = 'tegaki-size';
+ el.min = 1;
+ el.max = self.maxSize;
+ el.type = 'range';
+ lbl = T$.el('div');
+ lbl.className = 'tegaki-label';
+ lbl.textContent = TegakiStrings.size;
+ grp.appendChild(lbl);
+ T$.on(el, 'change', self.onSizeChange);
+ grp.appendChild(el);
+ ctrl.appendChild(grp);
+
+ // Alpha control
+ grp = T$.el('div');
+ grp.className = 'tegaki-ctrlgrp';
+ el = T$.el('input');
+ el.id = 'tegaki-alpha';
+ el.min = 0;
+ el.max = 1;
+ el.step = 0.01;
+ el.type = 'range';
+ lbl = T$.el('div');
+ lbl.className = 'tegaki-label';
+ lbl.textContent = TegakiStrings.alpha;
+ grp.appendChild(lbl);
+ T$.on(el, 'change', self.onAlphaChange);
+ grp.appendChild(el);
+ ctrl.appendChild(grp);
+
+ // Layer control
+ grp = T$.el('div');
+ grp.className = 'tegaki-ctrlgrp';
+ grp.id = 'tegaki-layer-grp';
+ el = T$.el('select');
+ el.id = 'tegaki-layer';
+ el.multiple = true;
+ el.size = 3;
+ lbl = T$.el('div');
+ lbl.className = 'tegaki-label';
+ lbl.textContent = TegakiStrings.layers;
+ grp.appendChild(lbl);
+ T$.on(el, 'change', self.onLayerChange);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.title = TegakiStrings.addLayer;
+ el.className = 'tegaki-icon tegaki-plus';
+ T$.on(el, 'click', self.onLayerAdd);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.title = TegakiStrings.delLayers;
+ el.className = 'tegaki-icon tegaki-minus';
+ T$.on(el, 'click', self.onLayerDelete);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.id = 'tegaki-layer-visibility';
+ el.title = TegakiStrings.showHideLayer;
+ el.className = 'tegaki-icon tegaki-eye';
+ T$.on(el, 'click', self.onLayerVisibilityChange);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.id = 'tegaki-layer-merge';
+ el.title = TegakiStrings.mergeLayers;
+ el.className = 'tegaki-icon tegaki-level-down';
+ T$.on(el, 'click', self.onMergeLayers);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.id = 'tegaki-layer-up';
+ el.title = TegakiStrings.moveLayerUp;
+ el.setAttribute('data-up', '1');
+ el.className = 'tegaki-icon tegaki-up-open';
+ T$.on(el, 'click', self.onMoveLayer);
+ grp.appendChild(el);
+ el = T$.el('span');
+ el.id = 'tegaki-layer-down';
+ el.title = TegakiStrings.moveLayerDown;
+ el.className = 'tegaki-icon tegaki-down-open';
+ T$.on(el, 'click', self.onMoveLayer);
+ grp.appendChild(el);
+ ctrl.appendChild(grp);
+
+ // Tool selector
+ grp = T$.el('div');
+ grp.className = 'tegaki-ctrlgrp';
+ el = T$.el('select');
+ el.id = 'tegaki-tool';
+ for (tool in Tegaki.tools) {
+ el2 = T$.el('option');
+ el2.value = tool;
+ el2.textContent = TegakiStrings[tool];
+ el.appendChild(el2);
+ }
+ lbl = T$.el('div');
+ lbl.className = 'tegaki-label';
+ lbl.textContent = TegakiStrings.tool;
+ grp.appendChild(lbl);
+ T$.on(el, 'change', self.onToolChange);
+ grp.appendChild(el);
+ ctrl.appendChild(grp);
+
+ cnt.appendChild(ctrl);
+
+ el = T$.el('div');
+ el.id = 'tegaki-menu-bar';
+
+ if (opts.canvasOptions) {
+ btn = T$.el('select');
+ btn.id = 'tegaki-canvas-select';
+ btn.title = TegakiStrings.changeCanvas;
+ btn.innerHTML = '<option value="0">' + TegakiStrings.blank + '</option>';
+ opts.canvasOptions(btn);
+ T$.on(btn, 'change', Tegaki.onCanvasSelected);
+ T$.on(btn, 'focus', Tegaki.onCanvasSelectFocused);
+ el.appendChild(btn);
+ }
+
+ btn = T$.el('span');
+ btn.className = 'tegaki-tb-btn';
+ btn.textContent = TegakiStrings.newCanvas;
+ T$.on(btn, 'click', Tegaki.onNewClick);
+ el.appendChild(btn);
+
+ btn = T$.el('span');
+ btn.className = 'tegaki-tb-btn';
+ btn.textContent = TegakiStrings.undo;
+ T$.on(btn, 'click', Tegaki.onUndoClick);
+ el.appendChild(btn);
+
+ btn = T$.el('span');
+ btn.className = 'tegaki-tb-btn';
+ btn.textContent = TegakiStrings.redo;
+ T$.on(btn, 'click', Tegaki.onRedoClick);
+ el.appendChild(btn);
+
+ btn = T$.el('span');
+ btn.className = 'tegaki-tb-btn';
+ btn.textContent = TegakiStrings.close;
+ T$.on(btn, 'click', Tegaki.onCancelClick);
+ el.appendChild(btn);
+
+ btn = T$.el('span');
+ btn.id = 'tegaki-finish-btn';
+ btn.className = 'tegaki-tb-btn';
+ btn.textContent = TegakiStrings.finish;
+ T$.on(btn, 'click', Tegaki.onDoneClick);
+ el.appendChild(btn);
+
+ cnt.appendChild(el);
+
+ bg = T$.el('div');
+ bg.id = 'tegaki';
+ self.bg = bg;
+ bg.appendChild(cnt);
+ document.body.appendChild(bg);
+ document.body.classList.add('tegaki-backdrop');
+
+ el = T$.el('canvas');
+ el.id = 'tegaki-ghost-layer';
+ el.width = canvas.width;
+ el.height = canvas.height;
+ self.ghostCanvas = el;
+ self.ghostCtx = el.getContext('2d');
+
+ self.cnt = cnt;
+ self.centerCnt();
+
+ self.canvas = canvas;
+
+ self.ctx = canvas.getContext('2d');
+ self.ctx.fillStyle = self.bgColor;
+ self.ctx.fillRect(0, 0, opts.width, opts.height);
+
+ self.addLayer();
+
+ self.setActiveLayer();
+
+ self.initTools();
+
+ self.setTool('pencil');
+
+ self.updateUI();
+
+ self.updateCursor();
+ self.updatePosOffset();
+
+ T$.on(self.bg, 'mousemove', self.onMouseMove);
+ T$.on(self.bg, 'mousedown', self.onMouseDown);
+ T$.on(self.layersCnt, 'contextmenu', self.onDummy);
+
+ T$.on(document, 'mouseup', self.onMouseUp);
+ T$.on(window, 'resize', self.updatePosOffset);
+ T$.on(window, 'scroll', self.updatePosOffset);
+ },
+
+ initTools: function() {
+ var tool;
+
+ for (tool in Tegaki.tools) {
+ (tool = Tegaki.tools[tool]) && tool.init && tool.init();
+ }
+ },
+
+ hexToRgb: function(hex) {
+ var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i);
+
+ if (c) {
+ return [
+ parseInt(c[1], 16),
+ parseInt(c[2], 16),
+ parseInt(c[3], 16)
+ ];
+ }
+
+ return null;
+ },
+
+ centerCnt: function() {
+ var aabb, cnt;
+
+ cnt = Tegaki.cnt;
+ aabb = cnt.getBoundingClientRect();
+
+ if (aabb.width > T$.docEl.clientWidth || aabb.height > T$.docEl.clientHeight) {
+ if (aabb.width > T$.docEl.clientWidth) {
+ cnt.classList.add('tegaki-overflow-x');
+ }
+ if (aabb.height > T$.docEl.clientHeight) {
+ cnt.classList.add('tegaki-overflow-y');
+ }
+ }
+ else {
+ cnt.classList.remove('tegaki-overflow-x');
+ cnt.classList.remove('tegaki-overflow-y');
+ }
+
+ cnt.style.marginTop = -Math.round(aabb.height / 2) + 'px';
+ cnt.style.marginLeft = -Math.round(aabb.width / 2) + 'px';
+ },
+
+ getCursorPos: function(e, axis) {
+ if (axis === 0) {
+ return e.clientX + window.pageXOffset + Tegaki.bg.scrollLeft - Tegaki.offsetX;
+ }
+ else {
+ return e.clientY + window.pageYOffset + Tegaki.bg.scrollTop - Tegaki.offsetY;
+ }
+ },
+
+ resume: function() {
+ Tegaki.bg.classList.remove('tegaki-hidden');
+ document.body.classList.add('tegaki-backdrop');
+
+ Tegaki.centerCnt();
+ Tegaki.updatePosOffset();
+
+ T$.on(document, 'mouseup', Tegaki.onMouseUp);
+ T$.on(window, 'resize', Tegaki.updatePosOffset);
+ T$.on(window, 'scroll', Tegaki.updatePosOffset);
+ },
+
+ hide: function() {
+ Tegaki.bg.classList.add('tegaki-hidden');
+ document.body.classList.remove('tegaki-backdrop');
+
+ T$.off(document, 'mouseup', Tegaki.onMouseUp);
+ T$.off(window, 'resize', Tegaki.updatePosOffset);
+ T$.off(window, 'scroll', Tegaki.updatePosOffset);
+ },
+
+ destroy: function() {
+ T$.off(Tegaki.bg, 'mousemove', Tegaki.onMouseMove);
+ T$.off(Tegaki.bg, 'mousedown', Tegaki.onMouseDown);
+ T$.off(Tegaki.layersCnt, 'contextmenu', Tegaki.onDummy);
+
+ T$.off(document, 'mouseup', Tegaki.onMouseUp);
+ T$.off(window, 'resize', Tegaki.updatePosOffset);
+ T$.off(window, 'scroll', Tegaki.updatePosOffset);
+
+ Tegaki.bg.parentNode.removeChild(Tegaki.bg);
+
+ TegakiHistory.clear();
+
+ document.body.classList.remove('tegaki-backdrop');
+
+ Tegaki.bg = null;
+ Tegaki.cnt = null;
+ Tegaki.canvas = null;
+ Tegaki.ctx = null;
+ Tegaki.layers = [];
+ Tegaki.layerIndex = 0;
+ Tegaki.activeCtx = null;
+ },
+
+ flatten: function() {
+ var i, layer, canvas, ctx;
+
+ canvas = T$.el('canvas');
+ canvas.width = Tegaki.canvas.width;
+ canvas.height = Tegaki.canvas.height;
+
+ ctx = canvas.getContext('2d');
+
+ ctx.drawImage(Tegaki.canvas, 0, 0);
+
+ for (i = 0; layer = Tegaki.layers[i]; ++i) {
+ if (layer.canvas.classList.contains('tegaki-hidden')) {
+ continue;
+ }
+ ctx.drawImage(layer.canvas, 0, 0);
+ }
+
+ return canvas;
+ },
+
+ updateUI: function(type) {
+ var i, ary, el, tool = Tegaki.tool;
+
+ ary = type ? [type] : ['size', 'alpha', 'color'];
+
+ for (i = 0; type = ary[i]; ++i) {
+ el = T$.id('tegaki-' + type);
+ el.value = type === 'color' ? Tegaki.toolColor : tool[type];
+
+ if (el.type === 'range') {
+ el.previousElementSibling.setAttribute('data-value', tool[type]);
+ }
+ }
+ },
+
+ rebuildLayerCtrl: function() {
+ var i, layer, sel, opt;
+
+ sel = T$.id('tegaki-layer');
+
+ sel.textContent = '';
+
+ for (i = Tegaki.layers.length - 1; layer = Tegaki.layers[i]; i--) {
+ opt = T$.el('option');
+ opt.value = layer.id;
+ opt.textContent = layer.name;
+ sel.appendChild(opt);
+ }
+ },
+
+ getColorAt: function(ctx, posX, posY) {
+ var rgba = ctx.getImageData(posX, posY, 1, 1).data;
+
+ return '#'
+ + ('0' + rgba[0].toString(16)).slice(-2)
+ + ('0' + rgba[1].toString(16)).slice(-2)
+ + ('0' + rgba[2].toString(16)).slice(-2);
+ },
+
+ renderCircle: function(r) {
+ var i, canvas, ctx, d, e, x, y, dx, dy, idata, data, c, color;
+
+ e = 1 - r;
+ dx = 0;
+ dy = -2 * r;
+ x = 0;
+ y = r;
+ d = 33;
+ c = 16;
+
+ canvas = T$.el('canvas');
+ canvas.width = canvas.height = d;
+ ctx = canvas.getContext('2d');
+ idata = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
+ data = idata.data;
+
+ color = 255;
+
+ data[(c + (c + r) * d) * 4 + 3] = color;
+ data[(c + (c - r) * d) * 4 + 3] = color;
+ data[(c + r + c * d) * 4 + 3] = color;
+ data[(c - r + c * d) * 4 + 3] = color;
+
+ while (x < y) {
+ if (e >= 0) {
+ y--;
+ dy += 2;
+ e += dy;
+ }
+
+ ++x;
+ dx += 2;
+ e += dx;
+
+ data[(c + x + (c + y) * d) * 4 + 3] = color;
+ data[(c - x + (c + y) * d) * 4 + 3] = color;
+ data[(c + x + (c - y) * d) * 4 + 3] = color;
+ data[(c - x + (c - y) * d) * 4 + 3] = color;
+ data[(c + y + (c + x) * d) * 4 + 3] = color;
+ data[(c - y + (c + x) * d) * 4 + 3] = color;
+ data[(c + y + (c - x) * d) * 4 + 3] = color;
+ data[(c - y + (c - x) * d) * 4 + 3] = color;
+ }
+
+ if (r > 0) {
+ for (i = 0; i < 3; ++i) {
+ data[(c + c * d) * 4 + i] = 127;
+ }
+ data[(c + c * d) * 4 + i] = color;
+ }
+
+ ctx.putImageData(idata, 0, 0);
+
+ return canvas;
+ },
+
+ setToolSize: function(size) {
+ Tegaki.tool.setSize && Tegaki.tool.setSize(size);
+ Tegaki.updateCursor();
+ },
+
+ setToolAlpha: function(alpha) {
+ Tegaki.tool.setAlpha && Tegaki.tool.setAlpha(alpha);
+ },
+
+ setToolColor: function(color) {
+ Tegaki.toolColor = color;
+ Tegaki.tool.setColor && Tegaki.tool.setColor(color);
+ Tegaki.updateCursor();
+ },
+
+ setTool: function(tool) {
+ tool = Tegaki.tools[tool];
+ Tegaki.tool = tool;
+ tool.set && tool.set();
+ },
+
+ debugDumpPixelData: function(canvas) {
+ var i, idata, data, len, out, el;
+
+ idata = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
+ data = idata.data;
+ len = data.length;
+
+ out = '';
+
+ for (i = 0; i < len; i += 4) {
+ out += data[i] + ' ' + data[i+1] + ' ' + data[i+2] + ' ' + data[i+3] + '%0a';
+ }
+
+ el = document.createElement('a');
+ el.href = 'data:,' + out;
+ el.download = 'dump.txt';
+ document.body.appendChild(el);
+ el.click();
+ document.body.removeChild(el);
+ },
+
+ debugDrawColors: function(sat) {
+ var i, ctx, grad, a;
+
+ Tegaki.resizeCanvas(360, 360);
+
+ ctx = Tegaki.activeCtx;
+ a = ctx.globalAlpha;
+ ctx.globalAlpha = 1;
+
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(0, 0, 360, 360);
+
+ for (i = 0; i < 360; ++i) {
+ if (sat) {
+ grad = ctx.createLinearGradient(0, 0, 10, 360);
+ grad.addColorStop(0, 'hsl(' + i + ', 0%, ' + '50%)');
+ grad.addColorStop(1, 'hsl(' + i + ', 100%, ' + '50%)');
+ ctx.strokeStyle = grad;
+ }
+ else {
+ ctx.strokeStyle = 'hsl(' + i + ', 100%, ' + '50%)';
+ }
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, 360);
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ if (!sat) {
+ grad = ctx.createLinearGradient(0, 0, 10, 360);
+ grad.addColorStop(0, '#000000');
+ grad.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, 360, 360);
+ }
+
+ ctx.globalAlpha = a;
+ },
+
+ onNewClick: function() {
+ var width, height, tmp;
+
+ width = prompt(TegakiStrings.promptWidth, Tegaki.canvas.width);
+ if (!width) { return; }
+
+ height = prompt(TegakiStrings.promptHeight, Tegaki.canvas.height);
+ if (!height) { return; }
+
+ width = +width;
+ height = +height;
+
+ if (width < 1 || height < 1) {
+ alert(TegakiStrings.badDimensions);
+ return;
+ }
+
+ tmp = {};
+ Tegaki.copyContextState(Tegaki.activeCtx, tmp);
+ Tegaki.resizeCanvas(width, height);
+ Tegaki.copyContextState(tmp, Tegaki.activeCtx);
+
+ TegakiHistory.clear();
+ Tegaki.centerCnt();
+ Tegaki.updatePosOffset();
+ },
+
+ onUndoClick: function() {
+ TegakiHistory.undo();
+ },
+
+ onRedoClick: function() {
+ TegakiHistory.redo();
+ },
+
+ onDoneClick: function() {
+ Tegaki.hide();
+ Tegaki.onDoneCb();
+ },
+
+ onCancelClick: function() {
+ if (!confirm(TegakiStrings.confirmCancel)) {
+ return;
+ }
+
+ Tegaki.destroy();
+ Tegaki.onCancelCb();
+ },
+
+ onColorChange: function() {
+ Tegaki.setToolColor(this.value);
+ },
+
+ onSizeChange: function() {
+ this.previousElementSibling.setAttribute('data-value', this.value);
+ Tegaki.setToolSize(+this.value);
+ },
+
+ onAlphaChange: function() {
+ this.previousElementSibling.setAttribute('data-value', this.value);
+ Tegaki.setToolAlpha(+this.value);
+ },
+
+ onLayerChange: function() {
+ var selectedOptions = T$.selectedOptions(this);
+
+ if (selectedOptions.length > 1) {
+ Tegaki.activeLayer = null;
+ }
+ else {
+ Tegaki.setActiveLayer(+this.value);
+ }
+ },
+
+ onLayerAdd: function() {
+ TegakiHistory.push(Tegaki.addLayer());
+ Tegaki.setActiveLayer();
+ },
+
+ onLayerDelete: function() {
+ var i, ary, sel, opt, selectedOptions, action;
+
+ sel = T$.id('tegaki-layer');
+
+ selectedOptions = T$.selectedOptions(sel);
+
+ if (Tegaki.layers.length === selectedOptions.length) {
+ return;
+ }
+
+ if (!confirm(TegakiStrings.confirmDelLayers)) {
+ return;
+ }
+
+ if (selectedOptions.length > 1) {
+ ary = [];
+
+ for (i = 0; opt = selectedOptions[i]; ++i) {
+ ary.push(+opt.value);
+ }
+ }
+ else {
+ ary = [+sel.value];
+ }
+
+ action = Tegaki.deleteLayers(ary);
+
+ TegakiHistory.push(action);
+ },
+
+ onLayerVisibilityChange: function() {
+ var i, ary, sel, opt, flag, selectedOptions;
+
+ sel = T$.id('tegaki-layer');
+
+ selectedOptions = T$.selectedOptions(sel);
+
+ if (selectedOptions.length > 1) {
+ ary = [];
+
+ for (i = 0; opt = selectedOptions[i]; ++i) {
+ ary.push(+opt.value);
+ }
+ }
+ else {
+ ary = [+sel.value];
+ }
+
+ flag = !Tegaki.getLayerById(ary[0]).visible;
+
+ Tegaki.setLayerVisibility(ary, flag);
+ },
+
+ onMergeLayers: function() {
+ var i, ary, sel, opt, selectedOptions, action;
+
+ sel = T$.id('tegaki-layer');
+
+ selectedOptions = T$.selectedOptions(sel);
+
+ if (selectedOptions.length > 1) {
+ ary = [];
+
+ for (i = 0; opt = selectedOptions[i]; ++i) {
+ ary.push(+opt.value);
+ }
+ }
+ else {
+ ary = [+sel.value];
+ }
+
+ if (ary.length < 2) {
+ alert(TegakiStrings.errorMergeOneLayer);
+ return;
+ }
+
+ if (!confirm(TegakiStrings.confirmMergeLayers)) {
+ return;
+ }
+
+ action = Tegaki.mergeLayers(ary);
+
+ TegakiHistory.push(action);
+ },
+
+ onMoveLayer: function(e) {
+ var id, action, sel;
+
+ sel = T$.id('tegaki-layer');
+
+ id = +sel.options[sel.selectedIndex].value;
+
+ if (action = Tegaki.moveLayer(id, e.target.hasAttribute('data-up'))) {
+ TegakiHistory.push(action);
+ }
+ },
+
+ onToolChange: function() {
+ Tegaki.setTool(this.value);
+ Tegaki.updateUI();
+ Tegaki.updateCursor();
+ },
+
+ onCanvasSelected: function() {
+ var img;
+
+ if (!confirm(TegakiStrings.confirmChangeCanvas)) {
+ this.selectedIndex = +this.getAttribute('data-current');
+ return;
+ }
+
+ if (this.value === '0') {
+ Tegaki.ctx.fillStyle = Tegaki.bgColor;
+ Tegaki.ctx.fillRect(0, 0, Tegaki.baseWidth, Tegaki.baseHeight);
+ }
+ else {
+ img = T$.el('img');
+ img.onload = Tegaki.onImageLoaded;
+ img.onerror = Tegaki.onImageError;
+ this.disabled = true;
+ img.src = this.value;
+ }
+ },
+
+ onImageLoaded: function() {
+ var el, tmp = {};
+
+ el = T$.id('tegaki-canvas-select');
+ el.setAttribute('data-current', el.selectedIndex);
+ el.disabled = false;
+
+ Tegaki.copyContextState(Tegaki.activeCtx, tmp);
+ Tegaki.resizeCanvas(this.naturalWidth, this.naturalHeight);
+ Tegaki.activeCtx.drawImage(this, 0, 0);
+ Tegaki.copyContextState(tmp, Tegaki.activeCtx);
+
+ TegakiHistory.clear();
+ Tegaki.centerCnt();
+ Tegaki.updatePosOffset();
+ },
+
+ onImageError: function() {
+ var el;
+
+ el = T$.id('tegaki-canvas-select');
+ el.selectedIndex = +el.getAttribute('data-current');
+ el.disabled = false;
+
+ alert(TegakiStrings.errorLoadImage);
+ },
+
+ resizeCanvas: function(width, height) {
+ var i, layer;
+
+ Tegaki.canvas.width = width;
+ Tegaki.canvas.height = height;
+ Tegaki.ghostCanvas.width = width;
+ Tegaki.ghostCanvas.height = height;
+
+ Tegaki.ctx.fillStyle = Tegaki.bgColor;
+ Tegaki.ctx.fillRect(0, 0, width, height);
+
+ for (i = 0; layer = Tegaki.layers[i]; ++i) {
+ Tegaki.layersCnt.removeChild(layer.canvas);
+ }
+
+ Tegaki.activeCtx = null;
+ Tegaki.layers = [];
+ Tegaki.layerIndex = 0;
+ T$.id('tegaki-layer').textContent = '';
+
+ Tegaki.addLayer();
+ Tegaki.setActiveLayer();
+ },
+
+ getLayerIndex: function(id) {
+ var i, layer, layers = Tegaki.layers;
+
+ for (i = 0; layer = layers[i]; ++i) {
+ if (layer.id === id) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ getLayerById: function(id) {
+ return Tegaki.layers[Tegaki.getLayerIndex(id)];
+ },
+
+ addLayer: function() {
+ var id, cnt, opt, canvas, layer, nodes, last;
+
+ canvas = T$.el('canvas');
+ canvas.className = 'tegaki-layer';
+ canvas.width = Tegaki.canvas.width;
+ canvas.height = Tegaki.canvas.height;
+
+ id = ++Tegaki.layerIndex;
+
+ layer = {
+ id: id,
+ name: 'Layer ' + id,
+ canvas: canvas,
+ ctx: canvas.getContext('2d'),
+ visible: true,
+ empty: true,
+ opacity: 1.0
+ };
+
+ Tegaki.layers.push(layer);
+
+ cnt = T$.id('tegaki-layer');
+ opt = T$.el('option');
+ opt.value = layer.id;
+ opt.textContent = layer.name;
+ cnt.insertBefore(opt, cnt.firstElementChild);
+
+ nodes = T$.cls('tegaki-layer', Tegaki.layersCnt);
+
+ if (nodes.length) {
+ last = nodes[nodes.length - 1];
+ }
+ else {
+ last = Tegaki.canvas;
+ }
+
+ Tegaki.layersCnt.insertBefore(canvas, last.nextElementSibling);
+
+ return new TegakiHistoryActions.AddLayer(id);
+ },
+
+ deleteLayers: function(ids) {
+ var i, id, len, sel, idx, indices, layers;
+
+ sel = T$.id('tegaki-layer');
+
+ indices = [];
+ layers = [];
+
+ for (i = 0, len = ids.length; i < len; ++i) {
+ id = ids[i];
+ idx = Tegaki.getLayerIndex(id);
+ sel.removeChild(sel.options[Tegaki.layers.length - 1 - idx]);
+ Tegaki.layersCnt.removeChild(Tegaki.layers[idx].canvas);
+
+ indices.push(idx);
+ layers.push(Tegaki.layers[idx]);
+
+ Tegaki.layers.splice(idx, 1);
+ }
+
+ Tegaki.setActiveLayer();
+
+ return new TegakiHistoryActions.DestroyLayers(indices, layers);
+ },
+
+ mergeLayers: function(ids) {
+ var i, id, sel, idx, canvasBefore, destId, dest, action;
+
+ sel = T$.id('tegaki-layer');
+
+ destId = ids.pop();
+ idx = Tegaki.getLayerIndex(destId);
+ dest = Tegaki.layers[idx].ctx;
+
+ canvasBefore = T$.copyCanvas(Tegaki.layers[idx].canvas);
+
+ for (i = ids.length - 1; i >= 0; i--) {
+ id = ids[i];
+ idx = Tegaki.getLayerIndex(id);
+ dest.drawImage(Tegaki.layers[idx].canvas, 0, 0);
+ }
+
+ action = Tegaki.deleteLayers(ids);
+ action.layerId = destId;
+ action.canvasBefore = canvasBefore;
+ action.canvasAfter = T$.copyCanvas(dest.canvas);
+
+ Tegaki.setActiveLayer(destId);
+
+ return action;
+ },
+
+ moveLayer: function(id, up) {
+ var idx, sel, opt, canvas, tmp, tmpId;
+
+ sel = T$.id('tegaki-layer');
+ idx = Tegaki.getLayerIndex(id);
+
+ canvas = Tegaki.layers[idx].canvas;
+ opt = sel.options[Tegaki.layers.length - 1 - idx];
+
+ if (up) {
+ if (!Tegaki.ghostCanvas.nextElementSibling) { return false; }
+ canvas.parentNode.insertBefore(canvas,
+ Tegaki.ghostCanvas.nextElementSibling.nextElementSibling
+ );
+ opt.parentNode.insertBefore(opt, opt.previousElementSibling);
+ tmpId = idx + 1;
+ }
+ else {
+ if (canvas.previousElementSibling.id === 'tegaki-canvas') { return false; }
+ canvas.parentNode.insertBefore(canvas, canvas.previousElementSibling);
+ opt.parentNode.insertBefore(opt, opt.nextElementSibling.nextElementSibling);
+ tmpId = idx - 1;
+ }
+
+ Tegaki.updateGhostLayerPos();
+
+ tmp = Tegaki.layers[tmpId];
+ Tegaki.layers[tmpId] = Tegaki.layers[idx];
+ Tegaki.layers[idx] = tmp;
+
+ Tegaki.activeLayer = tmpId;
+
+ return new TegakiHistoryActions.MoveLayer(id, up);
+ },
+
+ setLayerVisibility: function(ids, flag) {
+ var i, len, sel, idx, layer, optIdx;
+
+ sel = T$.id('tegaki-layer');
+ optIdx = Tegaki.layers.length - 1;
+
+ for (i = 0, len = ids.length; i < len; ++i) {
+ idx = Tegaki.getLayerIndex(ids[i]);
+ layer = Tegaki.layers[idx];
+ layer.visible = flag;
+
+ if (flag) {
+ sel.options[optIdx - idx].classList.remove('tegaki-strike');
+ layer.canvas.classList.remove('tegaki-hidden');
+ }
+ else {
+ sel.options[optIdx - idx].classList.add('tegaki-strike');
+ layer.canvas.classList.add('tegaki-hidden');
+ }
+ }
+ },
+
+ setActiveLayer: function(id) {
+ var ctx, idx;
+
+ idx = id ? Tegaki.getLayerIndex(id) : Tegaki.layers.length - 1;
+
+ if (idx < 0 || idx > Tegaki.maxLayers) {
+ return;
+ }
+
+ ctx = Tegaki.layers[idx].ctx;
+
+ if (Tegaki.activeCtx) {
+ Tegaki.copyContextState(Tegaki.activeCtx, ctx);
+ }
+
+ Tegaki.activeCtx = ctx;
+ Tegaki.activeLayer = idx;
+ T$.id('tegaki-layer').selectedIndex = Tegaki.layers.length - idx - 1;
+
+ Tegaki.updateGhostLayerPos();
+ },
+
+ updateGhostLayerPos: function() {
+ Tegaki.layersCnt.insertBefore(
+ Tegaki.ghostCanvas,
+ Tegaki.activeCtx.canvas.nextElementSibling
+ );
+ },
+
+ copyContextState: function(src, dest) {
+ var i, p, props = [
+ 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha',
+ 'lineWidth', 'globalCompositeOperation'
+ ];
+
+ for (i = 0; p = props[i]; ++i) {
+ dest[p] = src[p];
+ }
+ },
+
+ updateCursor: function() {
+ var radius;
+
+ radius = 0 | (Tegaki.tool.size / 2);
+
+ if (Tegaki.tool.noCursor || radius < 1) {
+ Tegaki.layersCnt.style.cursor = 'default';
+ return;
+ }
+
+ Tegaki.layersCnt.style.cursor = 'url("'
+ + Tegaki.renderCircle(radius).toDataURL('image/png')
+ + '") 16 16, default';
+ },
+
+ updatePosOffset: function() {
+ var aabb = Tegaki.canvas.getBoundingClientRect();
+ Tegaki.offsetX = aabb.left + window.pageXOffset + Tegaki.cnt.scrollLeft;
+ Tegaki.offsetY = aabb.top + window.pageYOffset + Tegaki.cnt.scrollTop;
+ },
+
+ onMouseMove: function(e) {
+ if (Tegaki.isPainting) {
+ Tegaki.tool.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1));
+ }
+ else if (Tegaki.isColorPicking) {
+ TegakiPipette.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1));
+ }
+ },
+
+ onMouseDown: function(e) {
+ if (e.target.parentNode === Tegaki.layersCnt) {
+ if (Tegaki.activeLayer === null) {
+ alert(TegakiStrings.noActiveLayer);
+ return;
+ }
+ if (!Tegaki.layers[Tegaki.activeLayer].visible) {
+ alert(TegakiStrings.hiddenActiveLayer);
+ return;
+ }
+ }
+ else if (e.target !== Tegaki.bg) {
+ return;
+ }
+
+ if (e.which === 3 || e.altKey) {
+ Tegaki.isColorPicking = true;
+ TegakiPipette.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1));
+ }
+ else {
+ Tegaki.isPainting = true;
+ TegakiHistory.pendingAction = new TegakiHistoryActions.Draw(
+ Tegaki.layers[Tegaki.activeLayer].id
+ );
+ TegakiHistory.pendingAction.addCanvasState(Tegaki.activeCtx.canvas, 0);
+ Tegaki.tool.draw(Tegaki.getCursorPos(e, 0), Tegaki.getCursorPos(e, 1), true);
+ }
+ },
+
+ onMouseUp: function(e) {
+ if (Tegaki.isPainting) {
+ Tegaki.tool.commit && Tegaki.tool.commit();
+ TegakiHistory.pendingAction.addCanvasState(Tegaki.activeCtx.canvas, 1);
+ TegakiHistory.push(TegakiHistory.pendingAction);
+ Tegaki.isPainting = false;
+ }
+ else if (Tegaki.isColorPicking) {
+ e.preventDefault();
+ Tegaki.isColorPicking = false;
+ }
+ },
+
+ onDummy: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+};
+
+// Bienvenido a Internet
+function topen() {
+ console.log(document.forms["imgform"].dataset.w);
+ console.log(document.forms["imgform"].dataset.h);
+ Tegaki.open({
+ onDone: function() {
+ document.getElementById('status').innerHTML = 'Subiendo...';
+ document.getElementById('buttons').style.display = 'none';
+ document.getElementById('links').style.display = 'none';
+ document.getElementById('filebase').value = Tegaki.flatten().toDataURL('image/png');
+ document.forms["imgform"].submit();
+ },
+ onCancel: function() { history.back(-1); },
+ width: document.forms["imgform"].dataset.w,
+ height: document.forms["imgform"].dataset.h
+ });
+}
+window.onload = function() {
+ document.getElementById('topen').addEventListener('click', topen);
+ topen();
+}; \ No newline at end of file
diff --git a/static/js/weabot.js b/static/js/weabot.js
new file mode 100644
index 0000000..0adc2e1
--- /dev/null
+++ b/static/js/weabot.js
@@ -0,0 +1,456 @@
+var style_cookie = 'weabot_style_ib';
+
+function set_stylesheet(styletitle) {
+ var css=document.getElementById("css");
+ if(css) {
+ css.href = "/static/css/" + styletitle.toLowerCase() + ".css";
+ localStorage.setItem(style_cookie, styletitle);
+ }
+}
+
+if(style_cookie && localStorage.hasOwnProperty(style_cookie)) {
+ set_stylesheet(localStorage.getItem(style_cookie));
+}
+
+var hiddenthreads = Array();
+/* IE/Opera fix, because they need to go learn a book on how to use indexOf with arrays */
+if (!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function(elt /*, from*/) {
+ var len = this.length;
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+ if (from < 0) from += len;
+ for (; from < len; from++) {
+ if (from in this && this[from] === elt) return from;
+ }
+ return -1;
+ };
+}
+
+function timeAgo(timestamp) {
+ var time = Math.round(Date.now()/1000);
+ var el = time - timestamp;
+ if (el==0) return "un instante";
+ else if (el==1) return "un segundo";
+ else if (el<60) return el + " segundos";
+ else if (el<120) return "un minuto";
+ else if (el<3600) return Math.round(el/60) + " minutos";
+ else if (el<7200) return "una hora";
+ else if (el<86400) return Math.round(el/3600) + " horas";
+ else if (el<172800) return "un día";
+ else if (el<2628000) return Math.round(el/86400) + " días";
+ else if (el<5256000) return "un mes";
+ else if (el<31536000) return Math.round(el/2628000) + " meses";
+ else if (el>31535999) return "más de un año";
+}
+
+function localTime(timestamp, id) {
+ id = id || 0;
+ var lcl = new Date(timestamp*1000);
+ lcl = ("0"+lcl.getDate()).slice(-2) + "/" + ("0" + (lcl.getMonth()+1)).slice(-2) + "/" + lcl.getFullYear().toString().slice(-2) + "(" + week[lcl.getDay()] + ")" + ("0"+lcl.getHours()).slice(-2) + ":" + ("0"+lcl.getMinutes()).slice(-2) + ":" + ("0"+lcl.getSeconds()).slice(-2)
+ if (id) lcl = lcl + " " + id;
+ return lcl;
+}
+
+function postClick(e) {
+ e.preventDefault();
+ var sel = window.getSelection().toString();
+ if (sel) sel = sel.replace(/^/gm, ">") + "\n";
+ insert(">>" + this.textContent + "\n" + sel);
+}
+
+function insert(text) {
+ var textarea=document.forms.postform.message;
+ if(textarea) {
+ if(textarea.createTextRange && textarea.caretPos) { // IE
+ var caretPos=textarea.caretPos;
+ caretPos.text=caretPos.text.charAt(caretPos.text.length-1)==" "?text+" ":text;
+ } else if(textarea.setSelectionRange) { // Firefox
+ var start=textarea.selectionStart;
+ var end=textarea.selectionEnd;
+ textarea.value=textarea.value.substr(0,start)+text+textarea.value.substr(end);
+ textarea.setSelectionRange(start+text.length,start+text.length);
+ } else { textarea.value+=text+" "; }
+ textarea.focus();
+ }
+ return false;
+}
+
+function quote(b, a) {
+ var v = eval("document." + a + ".message");
+ v.value += ">>" + b + "a\ndfs";
+ v.focus();
+}
+
+function checkhighlight() {
+ var match;
+ if(match=/#i([0-9]+)/.exec(document.location.toString()))
+ if(!document.forms.postform.message.value)
+ insert(">>"+match[1]+"\r\n");
+ if(match=/#([0-9]+)/.exec(document.location.toString()))
+ highlight(match[1]);
+}
+
+function highlight(post) {
+ var cells = document.getElementsByClassName("reply");
+ for(var i=0;i<cells.length;i++) if(cells[i].className == "reply highlight") cells[i].className = "reply";
+ var reply = document.getElementById("reply" + post);
+ if(reply) {
+ reply.className = "reply highlight";
+ var match = /^([^#]*)/.exec(document.location.toString());
+ document.location = match[1] + "#" + post;
+ }
+}
+
+function expandimg(e) {
+ var post_id = this.dataset.id;
+ var img_url = this.href;
+ var thumb_url = this.dataset.thumb;
+ var img_w = parseInt(this.dataset.w);
+ var img_h = parseInt(this.dataset.h);
+ var thumb_w = parseInt(this.dataset.tw);
+ var thumb_h = parseInt(this.dataset.th);
+ var format = img_url.substring(img_url.lastIndexOf(".")+1, img_url.length);
+ var exp_vid = 0;
+
+ if(window.innerWidth > 900) var ratio = Math.min((window.innerWidth-130) / img_w, 1);
+ else var ratio = Math.min((window.innerWidth-40) / img_w, 1);
+
+ if(thumb_w < 1) return true;
+
+ var img_cont = document.getElementById("thumb" + post_id);
+ var post_block = img_cont.parentElement.parentElement.getElementsByTagName("blockquote")[0];
+ var img;
+
+ for(var i = 0; i < img_cont.childNodes.length; i++)
+ if(img_cont.childNodes[i].nodeName.toLowerCase() == "img") {
+ img = img_cont.childNodes[i];
+ } else if(img_cont.childNodes[i].nodeName.toLowerCase() == "video") {
+ img = img_cont.childNodes[i];
+ exp_vid = 1;
+ }
+
+ if(img) {
+ if( (format == "webm") && (exp_vid == 0) ) var new_img = document.createElement("video");
+ else var new_img = document.createElement("img");
+ new_img.setAttribute("class", "thumb");
+ new_img.setAttribute("alt", "" + post_id);
+
+ if( (img.getAttribute("width") == ("" + thumb_w)) && (img.getAttribute("height") == ("" + thumb_h)) ) {
+ // thumbnail -> fullsize
+ if(format == "webm") {
+ new_img.setAttribute("controls", "");
+ new_img.setAttribute("loop", "");
+ new_img.setAttribute("autoplay", "");
+ }
+ new_img.setAttribute("src", "" + img_url);
+ new_img.setAttribute("width", img_w);
+ new_img.setAttribute("height", img_h);
+ new_img.setAttribute("style", "max-width:"+Math.floor((img_w*ratio))+"px;max-height:"+Math.floor((img_h*ratio))+"px;");
+ post_block.setAttribute("style", "");
+ img_cont.style.display = 'table';
+ } else {
+ // fullsize -> thumbnail
+ if(format == "webm") {
+ new_img.removeAttribute("controls");
+ new_img.removeAttribute("loop");
+ new_img.removeAttribute("autoplay");
+ }
+ new_img.setAttribute("src", "" + thumb_url);
+ new_img.setAttribute("width", thumb_w);
+ new_img.setAttribute("height", thumb_h);
+ post_block.setAttribute("style", "margin-left:"+(parseInt(thumb_w)+40)+"px;max-width:"+(1000-parseInt(thumb_w))+"px");
+ img_cont.removeAttribute("style");
+ }
+
+ while(img_cont.lastChild) img_cont.removeChild(img_cont.lastChild);
+
+ img_cont.appendChild(new_img);
+ e.preventDefault();
+ }
+}
+
+function filePreview(e) {
+ var file = postform.file.files[0];
+ var inpt = document.getElementById("file");
+ var prev = document.getElementById("filepreview");
+ var noimg = document.getElementById("noimage");
+ if (noimg) noimg = document.getElementById("noimage").parentNode;
+ if (file.size > (maxsize*1024)) { inpt.value = ""; return alert("El archivo es muy grande. El tamaño máximo es " + maxsize + " KB."); }
+ if (!types.includes(inpt.value.slice(inpt.value.lastIndexOf(".")+1).toUpperCase())) { inpt.value = ""; return alert("Tipo de archivo no soportado."); }
+ var read = new FileReader();
+ read.readAsDataURL(file);
+ read.onload = function(){
+ inpt.style.display = "none";
+ if (noimg) noimg.style.display = "none";
+ prev.removeAttribute("style");
+ var fname = (file.name.length < 20) ? file.name : file.name.substr(0, 19) + "…";
+ if (file.type.startsWith("image")) prev.insertAdjacentHTML('beforeend', '<img class="thumbpreview" src="' + read.result + '" /> ' + fname);
+ else prev.insertAdjacentHTML('beforeend', fname);
+ prev.appendChild(document.createTextNode(" ["));
+ var btn = document.createElement("a");
+ btn.href = "#";
+ btn.innerText = "Quitar"
+ btn.addEventListener("click", function(e) {
+ e.preventDefault();
+ prev.innerHTML = "";
+ prev.style.display = "none";
+ inpt.removeAttribute("style");
+ if (noimg) noimg.removeAttribute("style");
+ inpt.value = "";
+ });
+ prev.appendChild(btn);
+ prev.appendChild(document.createTextNode("] "));
+ };
+}
+
+function togglethread(e) {
+ e.preventDefault();
+ if(this.parentElement.id.startsWith("unhide")) {
+ var threadid = this.parentElement.id.substr(6);
+ } else if(this.parentElement.parentElement.id.startsWith("thread")) {
+ var threadid = this.parentElement.parentElement.id.substr(6);
+ } else { return; }
+ if (hiddenthreads.toString().indexOf(threadid) !== -1) {
+ document.getElementById("unhide" + threadid).style.display = "none";
+ document.getElementById("thread" + threadid).removeAttribute("style");
+ hiddenthreads.splice(hiddenthreads.indexOf(threadid), 1);
+ } else {
+ document.getElementById("unhide" + threadid).removeAttribute("style");
+ document.getElementById("thread" + threadid).style.display = "none";
+ hiddenthreads.push(threadid);
+ }
+ if (hiddenthreads == "") localStorage.removeItem("hiddenthreads");
+ else localStorage.setItem("hiddenthreads", hiddenthreads.join("!"));
+}
+
+function saveInputs(e) {
+ var e = e || window.event;
+ var form = e.target || e.srcElement;
+ if(typeof(form.fielda) !== "undefined") weabot.name = form.fielda.value;
+ if(typeof(form.fielda) !== "undefined") weabot.email = form.fieldb.value;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+}
+
+function setInputs(id) {
+ if (document.getElementById(id)) {
+ with(document.getElementById(id)) {
+ if(typeof(fielda) !== 'undefined' && !fielda.value && weabot.name) fielda.value = weabot.name;
+ if(typeof(fielda) !== 'undefined' && !fieldb.value && weabot.email) fieldb.value = weabot.email;
+ if(!password.value) password.value = getPassword();
+ addEventListener("submit", saveInputs);
+ }
+ }
+}
+
+function setPassword(id) {
+ if (document.getElementById(id).password)
+ with (document.getElementById(id)) if(!password.value) password.value = getPassword("weabot_password");
+}
+
+function getPassword() {
+ if (weabot.password) return weabot.password;
+ var char="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var pass="";
+ for (var i=0;i<8;i++) {
+ var rnd = Math.floor(Math.random()*char.length);
+ pass += char.substring(rnd, rnd+1);
+ }
+ weabot.password = pass;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+ return(pass);
+}
+
+function catSort(type) {
+ for(var i=0;i<srt.length;i++) srt[i].innerHTML=srt[i].innerText;
+ srt[type].innerHTML = "<b>"+srt[type].innerText+"</b>";
+ var cont = document.getElementById("catalog");
+ var elem = document.getElementsByClassName("thread");
+ var arr = Array.prototype.slice.call(elem);
+ if (type==0) arr.sort(function (a,b) { return (a.dataset.num-b.dataset.num) });
+ else if (type==1) arr.sort(function (a,b) { return (b.dataset.id-a.dataset.id) });
+ else if (type==2) arr.sort(function (a,b) { return (a.dataset.id-b.dataset.id) });
+ else if (type==3) arr.sort(function (a,b) { return (b.dataset.res-a.dataset.res) });
+ else if (type==4) arr.sort(function (a,b) { return (a.dataset.res-b.dataset.res) });
+ for (var j=0;j<arr.length;j++) cont.appendChild(arr[j]);
+ localStorage.setItem("catalog", JSON.stringify(opcs));
+}
+
+function catSearch() {
+ var filter = document.getElementById("cat_search").value.toLowerCase();
+ var nodes = document.getElementsByTagName("p");
+ for (i = 0; i < nodes.length; i++) {
+ if (nodes[i].innerText.toLowerCase().includes(filter))
+ nodes[i].parentNode.removeAttribute("style");
+ else nodes[i].parentNode.style.display = "none";
+ }
+}
+
+function catThumbs(big) {
+ var btn = document.getElementById("cat_size");
+ var cat = document.getElementById("catalog");
+ var threads = document.getElementsByClassName("thread");
+ if (opcs.big) {
+ cat.className = "enlarged";
+ for (j = 0; j < threads.length; j++) {
+ img = threads[j].getElementsByTagName("a")[0];
+ img.innerHTML = img.innerHTML.replace("/cat/", "/thumb/");
+ }
+ btn.innerText = "Grande";
+ } else {
+ cat.removeAttribute("class");
+ for (j = 0; j < threads.length; j++) {
+ img = threads[j].getElementsByTagName("a")[0];
+ img.innerHTML = img.innerHTML.replace("/thumb/", "/cat/");
+ }
+ btn.innerText = "Pequeño";
+ }
+ localStorage.setItem("catalog", JSON.stringify(opcs));
+}
+
+function catTeasers(teaser) {
+ var btn = document.getElementById("cat_hide");
+ if (!teaser) {
+ var style = document.createElement("style");
+ style.id = "teaser_style";
+ style.type = "text/css";
+ style.innerText = '#catalog p{display:none}';
+ document.head.appendChild(style);
+ btn.innerText = "Mostrar";
+ } else {
+ var style = document.getElementById("teaser_style");
+ if (style) style.parentNode.removeChild(style);
+ btn.innerText = "Ocultar";
+ }
+ localStorage.setItem("catalog", JSON.stringify(opcs));
+}
+
+function catMenu(e) {
+ var brd = document.getElementsByName("board")[0].value;
+ var id = this.dataset.id;
+ this.insertAdjacentHTML('afterbegin', '<div id="thread_ctrl" style="margin-bottom:3px;">[<a href="/cgi/report/' + brd + '/' + id + '">Denunciar</a>] [<a href="#" class="hh">Ocultar</a>]');
+ this.getElementsByClassName("hh")[0].addEventListener("click", function(e) {
+ document.getElementById("cat" + id + brd).style.display = "none";
+ hiddenthreads.push(id + brd);
+ localStorage.setItem("hiddenthreads", hiddenthreads.join("!"))
+ if (document.getElementById("hidden_label")) { hidden_num++; document.getElementById("hidden_num").innerText = hidden_num; }
+ else { hidden_num = 1; catHidden(); }
+ });
+}
+
+function catMenuClose(e) { document.getElementById("thread_ctrl").remove(); }
+
+function catHidden() {
+ var menu = document.getElementById("ctrl");
+ menu.insertAdjacentHTML('beforeend', ' <span id="hidden_label">[Hilos ocultos: <span id="hidden_num">' + hidden_num + '</span> - ');
+ var lbl = document.getElementById("hidden_label");
+ var shw = document.createElement("a");
+ shw.href = "#"; shw.innerText = "Deshacer";
+ shw.addEventListener("click", function(e) {
+ e.preventDefault();
+ for(var i=0;i<hiddenthreads.length;i++){
+ try {document.getElementById("cat" + hiddenthreads[i]).removeAttribute("style");}
+ catch(err) { continue; } }
+ lbl.parentElement.removeChild(lbl); hidden_num = 0;
+ var aux = hiddenthreads.join("!");
+ var exp = new RegExp("\\b[0-9]+" + document.getElementsByName("board")[0].value + "\\b", "g");
+ aux = aux.replace(exp, "!"); aux = aux.replace(/!+/g, "!"); aux = aux.replace(/(^!|!$)/g, "");
+ if (aux == "") { localStorage.removeItem("hiddenthreads"); hiddenthreads = []; }
+ else { localStorage.setItem("hiddenthreads", aux); hiddenthreads = aux.split("!"); }
+ });
+ lbl.appendChild(shw); lbl.appendChild(document.createTextNode("]"));
+}
+
+document.addEventListener("DOMContentLoaded", function(e) {
+ checkhighlight();
+ if (localStorage.hasOwnProperty("weabot")) weabot = JSON.parse(localStorage.getItem("weabot"));
+ else weabot = {"name":null,"email":null,"password":null};
+
+ if(localStorage.hasOwnProperty("hiddenthreads")) {
+ hiddenthreads = localStorage.getItem("hiddenthreads").split("!");
+ if (document.getElementById("catalog")) {
+ hidden_num = 0;
+ for(var i=0;i<hiddenthreads.length;i++){
+ try {
+ document.getElementById("cat" + hiddenthreads[i]).style.display = "none";
+ hidden_num++;
+ } catch(err) { continue; }
+ }
+ if (hidden_num) { catHidden(); }
+ } else {
+ for(var i=0;i<hiddenthreads.length;i++){
+ try {
+ document.getElementById("unhide" + hiddenthreads[i]).removeAttribute("style");
+ document.getElementById("thread" + hiddenthreads[i]).style.display = "none";
+ } catch(err) { continue; }
+ }
+ }
+ }
+
+ if(localStorage.getItem("shobon_time") != "false") {
+ var dts = document.getElementsByClassName("date");
+ if (dts[0].dataset.unix) {
+ week = ["dom", "lun", "mar", "mie", "jue", "vie", "sab"];
+ if (document.getElementsByName("board")[0].value == "2d") week = ["æ—¥", "月", "ç«", "æ°´", "木", "金", "土"];
+ for(var d=0;d<dts.length;d++) {
+ dts[d].addEventListener("mouseover", function(e) { this.title = "Hace " + timeAgo(this.dataset.unix); });
+ if (dts[d].innerText.includes("ID:")) var id = dts[d].innerText.split(" ")[1];
+ dts[d].innerText = localTime(dts[d].dataset.unix, id);
+ }
+ }
+ }
+
+ var ids = document.getElementsByClassName("postid");
+ for(var i=0;i<ids.length;i++) {
+ ids[i].addEventListener('click', postClick);
+ }
+ var tts = document.getElementsByClassName("tt");
+ for(var i=0;i<tts.length;i++) {
+ tts[i].addEventListener('click', togglethread);
+ }
+ var tts = document.getElementsByClassName("expimg");
+ for(var i=0;i<tts.length;i++) {
+ tts[i].addEventListener('click', expandimg);
+ }
+ var sss = document.getElementsByClassName("ss");
+ for(var i=0;i<sss.length;i++) {
+ sss[i].addEventListener('click', function() {
+ var cur = document.getElementById("cur_stl");
+ set_stylesheet(this.textContent);
+ if (cur) cur.removeAttribute("id");
+ this.id = "cur_stl";
+ });
+ if (sss[i].innerText == localStorage.getItem(style_cookie)) sss[i].id = "cur_stl";
+ }
+
+ if (document.getElementById(document.getElementsByName("board")[0].value))
+ document.getElementById(document.getElementsByName("board")[0].value).className = "cur_brd";
+
+ if (document.getElementById("postform")) {
+ setInputs("postform");
+ setPassword("delform");
+ maxsize = document.getElementById("maxsize").innerText;
+ types = document.getElementById("filetypes").innerText.split(", ");
+ if (types.includes("JPG")) types.push("JPEG");
+ postform.file.addEventListener("change", filePreview);
+ }
+
+ window.addEventListener("hashchange", checkhighlight);
+
+ if (document.getElementById("catalog")) {
+ srt = document.getElementsByClassName("cat_sort");
+ for(var i=0;i<srt.length;i++) { srt[i].innerHTML=srt[i].innerText; srt[i].addEventListener("click", function(e) { e.preventDefault(); opcs.sort=this.dataset.sort; catSort(opcs.sort); }); }
+ document.getElementById("cat_size").addEventListener("click", function(e) { e.preventDefault(); opcs.big=!opcs.big; catThumbs(opcs.big); });
+ document.getElementById("cat_hide").addEventListener("click", function(e) { e.preventDefault(); opcs.teaser=!opcs.teaser; catTeasers(opcs.teaser); });
+ document.getElementById("cat_search").addEventListener("keyup", catSearch);
+ if (localStorage.hasOwnProperty("catalog")) {
+ opcs = JSON.parse(localStorage.getItem("catalog"));
+ if(match=/\?sort=([0-9])/.exec(document.location.toString())) opcs.sort=match[1];
+ catSort(opcs.sort); catThumbs(opcs.big); catTeasers(opcs.teaser);
+ } else {
+ opcs = {"sort":1,"big":false,"teaser":true}; localStorage.setItem("catalog", JSON.stringify(opcs));
+ }
+ var thr = document.getElementsByClassName("thread");
+ for(var i=0;i<thr.length;i++) { thr[i].addEventListener("mouseenter", catMenu); thr[i].addEventListener("mouseleave", catMenuClose); }
+ }
+}); \ No newline at end of file
diff --git a/static/js/weabotxt.js b/static/js/weabotxt.js
new file mode 100644
index 0000000..37668d8
--- /dev/null
+++ b/static/js/weabotxt.js
@@ -0,0 +1,299 @@
+var style_cookie = "weabot_style_txt";
+if(style_cookie && localStorage.hasOwnProperty(style_cookie)) { set_stylesheet(localStorage.getItem(style_cookie)); }
+
+var hiddenposts = Array();
+
+function set_stylesheet(styletitle) {
+ var css=document.getElementById("css");
+ if(css) css.href = "/static/css/txt/"+styletitle.toLowerCase()+".css";
+ localStorage.setItem(style_cookie,styletitle);
+}
+
+function changeDate() {
+ var dts = document.getElementsByClassName("date");
+ if (dts[0].dataset.unix) {
+ week = ["dom", "lun", "mar", "mie", "jue", "vie", "sab"];
+ if (board == "world") week = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
+ for(var d=0;d<dts.length;d++) {
+ dts[d].addEventListener('mouseover', function(e) { this.title = "Hace " + timeAgo(this.dataset.unix); });
+ if (dts[d].innerText.includes("ID:")) var id = dts[d].innerText.split(" ")[1];
+ dts[d].innerText = localTime(dts[d].dataset.unix, id);
+ }
+ }
+}
+
+function timeAgo(timestamp) {
+ var time = Math.round(Date.now()/1000);
+ var el = time - timestamp;
+ if (el==0) return "un instante";
+ else if (el==1) return "un segundo";
+ else if (el<60) return el + " segundos";
+ else if (el<120) return "un minuto";
+ else if (el<3600) return Math.round(el/60) + " minutos";
+ else if (el<7200) return "una hora";
+ else if (el<86400) return Math.round(el/3600) + " horas";
+ else if (el<172800) return "un día";
+ else if (el<2628000) return Math.round(el/86400) + " días";
+ else if (el<5256000) return "un mes";
+ else if (el<31536000) return Math.round(el/2628000) + " meses";
+ else if (el>31535999) return "más de un año";
+}
+
+function localTime(timestamp, id) {
+ id = id || 0;
+ var lcl = new Date(timestamp*1000);
+ lcl = ("0"+lcl.getDate()).slice(-2) + "/" + ("0" + (lcl.getMonth()+1)).slice(-2) + "/" + lcl.getFullYear().toString().slice(-2) + "(" + week[lcl.getDay()] + ")" + ("0"+lcl.getHours()).slice(-2) + ":" + ("0"+lcl.getMinutes()).slice(-2) + ":" + ("0"+lcl.getSeconds()).slice(-2)
+ if (id) lcl = lcl + " " + id;
+ return lcl;
+}
+
+/* IE/Opera fix, because they need to go learn a book on how to use indexOf with arrays */
+if (!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function(elt /*, from*/) {
+ var len = this.length;
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+ if (from < 0) from += len;
+ for (; from < len; from++) { if (from in this && this[from] === elt) return from; }
+ return -1;
+ };
+}
+
+function getPassword() {
+ if (weabot.password) return weabot.password;
+ var char="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var pass="";
+ for (var i=0;i<8;i++) {
+ var rnd = Math.floor(Math.random()*char.length);
+ pass += char.substring(rnd, rnd+1);
+ }
+ weabot.password = pass;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+ return(pass);
+}
+
+function saveInputs(e) {
+ var e = e || window.event;
+ var form = e.target || e.srcElement;
+ if(typeof(form.fielda) !== "undefined") weabot.name = form.fielda.value;
+ if(typeof(form.fielda) !== "undefined") weabot.email = form.fieldb.value;
+ localStorage.setItem("weabot", JSON.stringify(weabot));
+}
+
+function setInputs(id) {
+ if (document.getElementById(id)) {
+ with(document.getElementById(id)) {
+ if(typeof(fielda) !== 'undefined' && !fielda.value && weabot.name) fielda.value = weabot.name;
+ if(typeof(fielda) !== 'undefined' && !fieldb.value && weabot.email) fieldb.value = weabot.email;
+ if(!password.value) password.value = getPassword();
+ if(typeof preview !== 'undefined') { preview.id = id; preview.addEventListener("click", previewPost); }
+ addEventListener("submit", saveInputs);
+ }
+ }
+}
+
+function setPassword(id) {
+ if (document.getElementById(id).password)
+ with (document.getElementById(id))
+ if(!password.value) password.value = getPassword("weabot_password");
+}
+
+// Textboard data
+function insert(text) {
+ var textarea=document.forms.postform.message;
+ if(textarea) {
+ if(textarea.createTextRange && textarea.caretPos) { // IE
+ var caretPos=textarea.caretPos;
+ caretPos.text=caretPos.text.charAt(caretPos.text.length-1)==" "?text+" ":text;
+ } else if(textarea.setSelectionRange) { // Firefox
+ var start=textarea.selectionStart;
+ var end=textarea.selectionEnd;
+ textarea.value=textarea.value.substr(0,start)+text+textarea.value.substr(end);
+ textarea.setSelectionRange(start+text.length,start+text.length);
+ } else {
+ textarea.value+=text+" ";
+ }
+ textarea.focus();
+ }
+ return false;
+}
+
+function deletePost(e) {
+ var ids = this.parentElement.firstChild.href.split("/");
+ var post = ids.pop();
+ var realid = ids.pop();
+ if(confirm("¿Seguro que deseas borrar el mensaje "+post+"?")) {
+ var script="/cgi/delete";
+ document.location=script+"?board="+board+"&password="+weabot.password+"&delete="+realid;
+ }
+ e.preventDefault();
+}
+
+function postClick(e) {
+ e.preventDefault();
+ var sel = window.getSelection().toString();
+ if (sel) { sel=sel.replace(/^/gm, ">")+"\n"; sel="\n"+sel; }
+ insert(">>" + this.textContent + sel);
+}
+
+function previewPost(e) {
+ var formid = e.target.id;
+ var thread = "0";
+ if(formid.startsWith("postform")) thread = formid.substr(8);
+
+ var form=document.getElementById(formid);
+ var preview=document.getElementById("preview"+thread);
+ var main=document.getElementById("options");
+
+ if(!form||!preview||!form.message.value) return;
+ if(main) main.style.display="";
+
+ preview.removeAttribute("style");
+ preview.innerHTML="<em>Cargando...</em>";
+
+ var text="message="+encodeURIComponent(form.message.value)+"&board="+board;
+ if (thread) text+="&parentid="+thread;
+
+ var xmlhttp=get_xmlhttp();
+ xmlhttp.open("POST", "/cgi/preview");
+ xmlhttp.onreadystatechange=function() { if(xmlhttp.readyState==4) preview.innerHTML=xmlhttp.responseText; }
+ if(is_ie()||xmlhttp.setRequestHeader) xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
+ xmlhttp.send(text);
+}
+
+function listSort() {
+ var s = opcs.sort;
+ var cont = document.getElementById("content");
+ var elem = document.getElementsByClassName("row");
+ var arr = Array.prototype.slice.call(elem); arr.shift();
+ if (s=="Normal") { arr.sort(function (a,b) { return (parseInt(a.childNodes[1].innerText) - parseInt(b.childNodes[1].innerText)) }); }
+ else if(s=="Edad"){ arr.sort(function (a,b) { return (b.children[1].firstChild.href.split("/")[5] - a.children[1].firstChild.href.split("/")[5]) }); }
+ else if(s=="Largo"){ arr.sort(function (a,b) { return (b.children[2].textContent - a.children[2].textContent) }); }
+ else if(s=="Rapidez"){ var now=Math.round(Date.now()/1000); arr.sort(function (a,b) { return ((b.children[2].textContent/(now-b.children[1].firstChild.href.split("/")[5])) - (a.children[2].textContent/(now-a.children[1].firstChild.href.split("/")[5]))) }); }
+ else if(s=="Aleatorio"){ arr.sort(function(a,b) { return 0.5-Math.random()}); }
+ for (var j=0;j<arr.length;j++) cont.appendChild(arr[j]);
+ localStorage.setItem("threadlist", JSON.stringify(opcs));
+}
+
+function listDisplay() {
+ var d = opcs.disp;
+ if (d=="Malla") {
+ document.getElementById("header").style.display = "none";
+ document.getElementById("content").className = "grid";
+ var style = document.createElement("style");
+ style.id = "labels";
+ style.type = "text/css";
+ style.innerText = '#content .row div.date{display:none}#content.grid div.com:before{content:"("}#content.grid .com:after{content:")"}';
+ document.head.appendChild(style);
+ } else if (d=="Lista") {
+ var style = document.getElementById("labels");
+ if (style) style.parentNode.removeChild(style);
+ document.getElementById("header").removeAttribute("style");
+ document.getElementById("content").className = "list";
+ }
+ localStorage.setItem("threadlist", JSON.stringify(opcs));
+}
+
+function searchSubjects(e) {
+ var filter = this.value.toLowerCase();
+ var nodes = document.getElementsByClassName("thread");
+ for (i=0; i<nodes.length; i++) {
+ if (nodes[i].innerText.toLowerCase().includes(filter)) nodes[i].parentNode.removeAttribute("style");
+ else nodes[i].parentNode.style.display = "none";
+ }
+}
+
+function get_xmlhttp() {
+ var xmlhttp;
+ try { xmlhttp=new ActiveXObject("Msxml2.XMLHTTP"); }
+ catch(e1) {
+ try { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); }
+ catch(e1) { xmlhttp=null; }
+ }
+ if(!xmlhttp && typeof XMLHttpRequest!='undefined') xmlhttp=new XMLHttpRequest();
+ return(xmlhttp);
+}
+
+function is_ie() { return(document.all&&!document.opera); }
+
+function showpost(post) {
+ post.children[0].classList.remove("hidden");
+ post.children[1].style.display = 'block';
+}
+function hidepost(post) {
+ post.children[0].classList.add("hidden");
+ post.children[1].style.display = 'none';
+}
+function togglepost(e) {
+ e.preventDefault();
+ var post = this.parentElement;
+ var postid = board + this.getElementsByClassName("date")[0].dataset.unix;
+
+ if(post.children[1].style.display == 'none') {
+ showpost(post);
+ if(hiddenposts.includes(postid)) hiddenposts.splice(hiddenposts.indexOf(postid), 1);
+ } else {
+ hidepost(post);
+ if(!hiddenposts.includes(postid)) hiddenposts.push(postid);
+ }
+
+ localStorage.setItem("hiddenposts", hiddenposts.join("!"));
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ if (localStorage.hasOwnProperty("weabot")) weabot = JSON.parse(localStorage.getItem("weabot"));
+ else weabot = {"name":null,"email":null,"password":null};
+
+ board = document.getElementsByName("board")[0].value;
+
+ if(localStorage.getItem("shobon_time") != "false") {
+ changeDate();
+ }
+
+ var ids = document.getElementsByClassName("num");
+ for(var i=0;i<ids.length;i++) ids[i].addEventListener('click', postClick);
+
+ var forms = document.getElementsByTagName("form");
+ for(var i=0;i<forms.length;i++) { if(forms[i].id.startsWith("postform")) setInputs(forms[i].id); }
+
+ var sss = document.getElementsByClassName("ss");
+ var style = localStorage.getItem(style_cookie);
+ for(var i=0;i<sss.length;i++) {
+ sss[i].addEventListener("click", function() {
+ set_stylesheet(this.textContent);
+ var cur = document.getElementById("cur_stl");
+ if (cur) cur.removeAttribute("id");
+ this.id = "cur_stl";
+ });
+ if (sss[i].innerText == style) sss[i].id = "cur_stl";
+ }
+
+ var dds = document.getElementsByClassName("del");
+ for(var i=0;i<dds.length;i++) { dds[i].children[1].addEventListener("click", deletePost); }
+
+ var where = document.getElementById(document.getElementsByName("board")[0].value);
+ if (where) where.className = "cur_brd";
+
+ if (document.body.className == "threads") {
+ var srt = document.getElementsByClassName("l_s");
+ for(var i=0;i<srt.length;i++){ srt[i].addEventListener("click",function(e){ e.preventDefault(); opcs.sort=this.textContent; listSort(); }); }
+ var dsp = document.getElementsByClassName("l_d");
+ for(var i=0;i<dsp.length;i++){ dsp[i].addEventListener("click",function(e){ e.preventDefault(); opcs.disp=this.textContent; listDisplay(); }); }
+ document.getElementById("l_sr").addEventListener("keyup", searchSubjects);
+ if (localStorage.hasOwnProperty("threadlist")) { opcs = JSON.parse(localStorage.getItem("threadlist")); listSort(); listDisplay(); }
+ else { opcs = {"sort":"Normal","disp":"Lista"}; localStorage.setItem("threadlist", JSON.stringify(opcs)); }
+ }
+
+ if(localStorage.hasOwnProperty("hiddenposts"))
+ hiddenposts = localStorage.getItem("hiddenposts").split("!");
+
+ var pps = document.getElementsByClassName("date");
+ for(var i=0;i<pps.length;i++) {
+ if(hiddenposts.includes(board+pps[i].dataset.unix)) {
+ console.log(pps[i].dataset.unix);
+ hidepost(pps[i].parentElement.parentElement);
+ }
+
+ pps[i].parentElement.addEventListener('dblclick', togglepost);
+ }
+}); \ No newline at end of file
diff --git a/static/js/wpaint/.gitignore b/static/js/wpaint/.gitignore
new file mode 100644
index 0000000..7a48116
--- /dev/null
+++ b/static/js/wpaint/.gitignore
@@ -0,0 +1,3 @@
+test/uploads/wPaint-*
+node_modules/
+npm-debug.txt \ No newline at end of file
diff --git a/static/js/wpaint/README.md b/static/js/wpaint/README.md
new file mode 100644
index 0000000..eac4c2a
--- /dev/null
+++ b/static/js/wpaint/README.md
@@ -0,0 +1,421 @@
+# wPaint.js
+
+A jQuery paint plugin for a simple drawing surface that you can easily pop into your pages, similar to the basic windows paint program.
+
+* [View the wPaint demo](http://wpaint.websanova.com)
+* [Download the lastest version of wPaint](https://github.com/websanova/wPaint/tags)
+
+
+## Related Plugins
+
+* [wScratchPad](http://wscratchpad.websanova.com) - Plugin simulating scratch card.
+* [wColorPicker](http://wcolorpicker.websanova.com) - Color pallette seleciton plugin.
+
+
+## Support
+
+If you enjoy this plugin please leave a small contribution on [Gittip](https://gittip.com/websanova). I work on these plugins completely in my free time and any contribution is greatly appreciated.
+
+
+## Settings
+
+Settings are available per plugin. Meaning only when that plugin is included will those settings be available.
+
+### core
+
+```js
+$.fn.wPaint.defaults = {
+ path: '/', // set absolute path for images and cursors
+ theme: 'standard classic', // set theme
+ autoScaleImage: true, // auto scale images to size of canvas (fg and bg)
+ autoCenterImage: true, // auto center images (fg and bg, default is left/top corner)
+ menuHandle: true, // setting to false will means menus cannot be dragged around
+ menuOrientation: 'horizontal', // menu alignment (horizontal,vertical)
+ menuOffsetLeft: 5, // left offset of primary menu
+ menuOffsetTop: 5, // top offset of primary menu
+ bg: null, // set bg on init
+ image: null, // set image on init
+ imageStretch: false, // stretch smaller images to full canvans dimensions
+ onShapeDown: null, // callback for draw down event
+ onShapeMove: null, // callback for draw move event
+ onShapeUp: null // callback for draw up event
+};
+```
+
+### main
+
+```js
+$.extend($.fn.wPaint.defaults, {
+ mode: 'pencil', // set mode
+ lineWidth: '3', // starting line width
+ fillStyle: '#FFFFFF', // starting fill style
+ strokeStyle: '#FFFF00' // start stroke style
+});
+```
+
+### text
+
+```js
+$.extend($.fn.wPaint.defaults, {
+ fontSize : '12', // current font size for text input
+ fontFamily : 'Arial', // active font family for text input
+ fontBold : false, // text input bold enable/disable
+ fontItalic : false, // text input italic enable/disable
+ fontUnderline : false // text input italic enable/disable
+});
+```
+
+### shapes
+
+No settings.
+
+### file
+
+Note that the callbacks for `file` are user generated for the most part as they deal heavily with client/server side code. You can view the demo code to get a feeling for how they might be setup.
+
+```js
+$.extend($.fn.wPaint.defaults, {
+ saveImg: null, // callback triggerd on image save
+ loadImgFg: null, // callback triggered on image fg
+ loadImgBg: null // callback triggerd on image bg
+});
+```
+
+
+## Examples
+
+To start, you will need to include any dependencies (the paths and versions may differ):
+```html
+<!-- jQuery -->
+<script type="text/javascript" src="./lib/jquery.1.10.2.min.js"></script>
+<!-- jQuery UI -->
+<script type="text/javascript" src="./lib/jquery.ui.core.1.10.3.min.js"></script>
+<script type="text/javascript" src="./lib/jquery.ui.widget.1.10.3.min.js"></script>
+<script type="text/javascript" src="./lib/jquery.ui.mouse.1.10.3.min.js"></script>
+<script type="text/javascript" src="./lib/jquery.ui.draggable.1.10.3.min.js"></script>
+<!-- wColorPicker -->
+<link rel="Stylesheet" type="text/css" href="./lib/wColorPicker.min.css" />
+<script type="text/javascript" src="./lib/wColorPicker.min.js"></script>
+```
+
+
+
+Then you need to include the wPaint core files:
+```html
+<link rel="Stylesheet" type="text/css" href="./wPaint.min.css" />
+<script type="text/javascript" src="./wPaint.min.js"></script>
+```
+
+From here we will need to include plugin files for whatever menu icons we would like to support. This can include the plugins provided with the release of this plugin or any additional plugins that you can write on your own.
+
+```html
+<script type="text/javascript" src="./plugins/main/wPaint.menu.main.min.js"></script>
+<script type="text/javascript" src="./plugins/text/wPaint.menu.text.min.js"></script>
+<script type="text/javascript" src="./plugins/shapes/wPaint.menu.main.shapes.min.js"></script>
+<script type="text/javascript" src="./plugins/file/wPaint.menu.main.file.min.js"></script>
+```
+
+
+### path
+
+If you are putting wPaint into a path other than root (most likely you will) then you will need to set the `path` option since the image and cursor icon paths are set in the JavaScript and not in CSS. This means we can not make them relative from the included file like we can in the CSS file but rather relative to the dispalying page. The default path is just the root folder `/` but a path can be set for wpaint.
+
+```js
+$('#wPaint').wPaint({
+ path: '/js/lib/wPaint/'
+});
+```
+
+
+### save / load
+
+There have been many questions regarding saving / loading images using wPaint. Loading images CANNOT be done locally or from other domains due to browser restrictions with [cross origin](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Same_origin_policy_for_JavaScript) policies. There are some potential workarounds for this using [CORS](https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image?redirectlocale=en-US&redirectslug=CORS_Enabled_Image) but this has not been implemented yet.
+
+As it stands the best approach to this is to first upload the image to your server, save it, then use the url from your server to set images in the future. You can check the `upload.php` file in the `test` folder with a php example for saving an image. However note that saving the image on your server will depend on the language/framework being used.
+
+
+### themes
+
+With this release multiple themeing has been introduced by simply space separating multiple theme keywords. For example 'standard classic'. This is to allow us to theme colours and sizes separately and use them interchangeably.
+
+```js
+$("#elem").wPaint({
+ theme: 'standard classic'
+});
+```
+
+
+### image data
+
+Set image on the fly. This can be a base64 encoded image or simply a path to any image on the same domain.
+
+```js
+$('#wPaint').wPaint('image', '<image_data>');
+```
+
+Get the image data:
+
+```js
+var imageData = $("#wPaint").wPaint("image");
+
+$("#canvasImage").attr('src', imageData);
+```
+
+
+### callbacks
+
+There are three callbacks available for each drawing operation of `down`, `move`, and `up`. Available is the `this` object which refers to the `wPaint` object and gives access to all internal functions.
+
+```js
+$("#elem").wPaint({
+ onDrawDown: function (e) {
+ console.log(this.settings.mode + ": " + e.pageX + ',' + e.pageY);
+ },
+ onDrawMove: function (e) {
+ console.log(this.settings.mode + ": " + e.pageX + ',' + e.pageY);
+ },
+ onDrawUp: function (e) {
+ console.log(this.settings.mode + ": " + e.pageX + ',' + e.pageY);
+ }
+});
+```
+
+
+### image / bg
+
+Set the image or background with an image or color at any time.
+
+```js
+$("#wPaint").wPaint({
+ image: './some/path/imagepreload.png',
+ bg: '#ff0000'
+});
+```
+
+
+### resize
+
+In case you want to resize your canvas there is a `resize` utility function available. Call this after you change the dimensions of your canvas element. Check the `test/fullscreen.html` demo for sample code.
+
+```js
+$("#wPaint").wPaint('resize');
+```
+
+
+### clear
+
+Clear the canvas manually.
+
+```javascript
+$('#wPaint').wPaint('clear');
+```
+
+
+### undo / redo
+
+We can also manually run an `undo` or `redo`.
+
+```javascript
+$('#wPaint').wPaint('undo');
+$('#wPaint').wPaint('redo');
+```
+
+
+## Extending
+
+With version 2.0 wPaint can now easily be extended by setting all or some of the following properties:
+
+```js
+// add menu
+$.fn.wPaint.menus.main = {
+ img: '/plugins/main/img/icons-menu-main.png',
+ items: {
+ undo: {
+ icon: 'generic',
+ title: 'Undo',
+ index: 0,
+ callback: function () { this.undo(); }
+ }
+}
+
+// extend cursors
+$.extend($.fn.wPaint.cursors, {
+ pencil: 'url("/plugins/main/img/cursor-pencil.png") 0 11.99, default',
+});
+
+// extend defaults
+$.extend($.fn.wPaint.defaults, {
+ mode: 'pencil', // set mode
+ lineWidth: '3', // starting line width
+ fillStyle: '#FFFFFF', // starting fill style
+ strokeStyle: '#FFFF00' // start stroke style
+});
+
+// extend functions
+$.fn.wPaint.extend({
+ undo: function () {
+ if (this.undoArray[this.undoCurrent - 1]) {
+ this._setUndo(--this.undoCurrent);
+ }
+
+ this._undoToggleIcons();
+ }
+});
+```
+
+
+### overriding
+
+When calling the `$.fn.wPaint.extend()` function the values for functions will not override the existing functions but just extend them with duck punching technique. This means the original funciton will always run followed by your extended function.
+
+This allows us to just string multiple `generate` or `init` functions together and not have to worry about overwriting any code.
+
+
+
+### menus
+
+The first menu appended will always automatically become the `primary` menu meaning it is the one displayed on init. All other menus will become `secondary` menus meaning they are toggled by icons.
+
+We can extend, modify or add items in the menu by updating the object we want. So for instance if we want to add a new icon to the main menu we could just do:
+
+```js
+$.fn.wPaint.menus.main.items.undo = {
+ // set properties here
+};
+```
+
+Likewise we can overwrite or add properties to an existing object. For instance below we modified the title and added the `after` property to change the position in which the `undo` icon will appear in the menu.
+
+```js
+$.fn.wPaint.menus.main.items.undo = {
+ title: 'Undo at your own risk',
+ after: 'clear'
+};
+```
+
+
+### icon properties
+
+Below is just a sample to list all possible icon properties. Note that the icon name such as `undo` is the `key` name used for CSS styling and internal naming.
+
+```js
+undo: {
+
+ // The icon sets the type of icon we want to generate
+ // below are the available types that come out of the box.
+ //
+ // generic: just runs a callback and nothing else
+ // activate: runs callback and activates (highlights)
+ // colorPicker: generates a color picker
+ // select: generates a select box (list)
+ // toggle: toggles on/off returns true or false
+ // menu: toggles secondary menu (icon/menu name must match)
+ icon: 'generic',
+
+ // Set a group for an icon turning it into a stacked
+ // group select (list). All icons with this group name will
+ // be appended to that select list. If not set the icon will
+ // just be standalone.
+ group: null,
+
+ // Set placement of icon in reference to another icon
+ after: 'clear',
+
+ // Title displayed on hover.
+ title: 'Undo',
+
+ // set an alternate image path to use for this icon
+ img: '/som/path.png',
+
+ // Index position in image file starting from 0
+ index: 0,
+
+ // a range of values to use for a select icon
+ range: [1, 2, 3, 4, 5],
+
+ // User range will set the value of the range as the
+ // css property based on the name of the icon. For instance
+ // if the icon is fontFamily that css property will get set.
+ useRange: true,
+
+ // The default value to set for this icon. This of course
+ // can be overridden using `set` calls on init.
+ value: 3,
+
+ // Callback when icon is clicked.
+ callback: function () { this.undo(); }
+}
+```
+
+If you want to create a new icon type you will need to extend wPaint to include processing for this new icon. A funciton in the form below should be written:
+
+```js
+_createIconType: function (item) {
+
+ // Get your started with a base icon.
+ var $icon = this._createIconBase(item);
+
+ // Return the icon with whatever functionality
+ // you want to add to it.
+ return $icon;
+}
+```
+
+
+### icon images
+
+Images for each plugin should be kept in one file and can be either specificed by the `img` value on the top level and can be overriden at the icon level. Each icon should also specify an index value as to the position of the icon in the image starting from 0. Icons should alll be the same size and dimensions should be set in the `size` theme.
+
+```js
+$.fn.wPaint.menus.main.items.undo = {
+ img: '/plugins/main/img/icons-menu-main.png',
+ items: {
+ undo: {
+ icon: 'generic',
+ title: 'Undo',
+ img: '/some/other/path.png'
+ index: 0,
+ callback: function () { this.undo(); }
+ }
+}
+```
+
+
+### cursors
+
+There is now a master `cursors` object used to store cursor references.
+
+$.extend($.fn.wPaint.cursors, {
+ pencil: 'url("/plugins/main/img/cursor-pencil.png") 0 11.99, default',
+});
+
+We can sepcify the cursor to use by calling `setCursor()` and passing the cursor name to use. Note that this is a set function and we can set the cursor at any time.
+
+```js
+$('#wPaint').wPaint('cursor', 'rocket');
+```
+
+Note that when you are setting the position of the cursor never set it to the exact dimension. For instance if the iamge is `12x12` and you want it's position to be `12` set it to `11.99`. This is do to some strange bug in Chrome which will not position the curosr if set exactly.
+
+
+## Thanks
+
+Thanks to everyone who has contribute code in the previous version and has showed interest in the plugin. Below is some thanks and attribution for code used in this plugin (if I left you out please let me know).
+
+* [Rounded corners and extending Canvas with new shapes](http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html)
+* [Nice efficient algorithm for fill tool](http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool)
+
+
+## Resources
+
+* [More jQuery plugins by Websanova](http://websanova.com/plugins)
+* [Websanova JavaScript Extensions Project](http://websanova.com/extensions)
+* [jQuery Plugin Development Boilerplate](http://wboiler.websanova.com)
+* [The Ultimate Guide to Writing jQuery Plugins](http://www.websanova.com/blog/jquery/the-ultimate-guide-to-writing-jquery-plugins)
+
+
+## License
+
+MIT licensed
+
+Copyright (C) 2011-2012 Websanova http://www.websanova.com
diff --git a/static/js/wpaint/bai.js b/static/js/wpaint/bai.js
new file mode 100644
index 0000000..6b436c1
--- /dev/null
+++ b/static/js/wpaint/bai.js
@@ -0,0 +1,23 @@
+function saveImg(image) {
+ var _this = this;
+ var url = document.getElementById('finish').href;
+
+ $.ajax({
+ type: 'POST',
+ url: '/oek_temp/upload.php',
+ data: {image: image},
+ success: function (resp) {
+ _this._displayStatus('Image saved successfully');
+ window.location.href = url;
+ }
+ });
+}
+
+$('#wPaint').wPaint({
+ path: '/static/js/wpaint/',
+ bg: '#ffffff',
+ menuOffsetLeft: -35,
+ menuOffsetTop: -50,
+ menuOrientation: 'horizontal',
+ saveImg: saveImg
+}); \ No newline at end of file
diff --git a/static/js/wpaint/demo/demo.css b/static/js/wpaint/demo/demo.css
new file mode 100644
index 0000000..ad8a418
--- /dev/null
+++ b/static/js/wpaint/demo/demo.css
@@ -0,0 +1,266 @@
+/*** layout ***/
+
+body, html {
+ font-family: verdana;
+ font-size: 12px;
+ line-height:20px;
+ color: #333;
+ background-color: #FEFEFE;
+ margin: 0;
+ text-rendering: optimizeLegibility;
+}
+a {
+ color: #3399FF;
+ text-decoration: none;
+}
+code {
+ padding: 1px 3px;
+ color: #D14;
+ background-color: #F7F7F9;
+ border: 1px solid #E1E1E8;
+ border-radius: 5px;
+}
+
+h2 {
+ display: inline-block;
+}
+
+.no-margin-top {
+ margin-top: 0px;
+}
+
+/*** header ***/
+
+header {
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 40px;
+ line-height: 40px;
+ background-color: #1B1B1B;
+ background-image: linear-gradient(to bottom, #222222, #111111);
+ background-repeat: repeat-x;
+ border-color: #252525;
+ box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
+ border-width: 0 0 1px;
+ color: #FFF;
+ font-size: 10px;
+ text-align: left;
+ vertical-align: middle;
+ z-index: 100;
+}
+#header-logo {
+ position: absolute;
+ display: block;
+ width: 94px;
+ height: 20px;
+ left: 10px;
+ top: 10px;
+ background-image: url('./img/websanova-logo-small-full-black.png');
+}
+#header-links {
+ display: none;
+ position: absolute;
+ width: 100%;
+ left: 0px;
+ top: 40px;
+}
+#header-links a {
+ display: block;
+ font-size: 9px;
+ font-weight: bold;
+ font-family: Verdana;
+ color: #FAFAFA;
+ text-transform: uppercase;
+ margin-left: 0px;
+ text-align: center;
+ background-color: #1B1B1B;
+ border-top: solid 1px #CACACA;
+ line-height: 14px;
+ padding: 10px;
+}
+#header-links a:hover {
+ color: #636363;
+}
+
+/*** menu ***/
+
+#header-menu-button {
+ position: absolute;
+ right: 6px;
+ top: 6px;
+ display: block;
+ color: #757575;
+ background-color: #e1e1e1;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -ms-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -webkit-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -o-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: linear-gradient(top, #ebebeb, #e1e1e1);
+ box-shadow: inset 0 0 8px 2px #c6c6c6, 0 1px 0 0 #f4f4f4;
+ border: none;
+ border-radius: 3px;
+ -webkit-border-radius: 3px;
+ padding: 6px 10px;
+ font-size: 11px;
+ line-height: 16px;
+ cursor: pointer;
+}
+
+/*** content ***/
+
+#plugin-name {
+ padding-bottom: 30px;
+ font-size: 24px;
+ border-bottom: solid #CACACA 1px;
+}
+#github-button {
+ display: block;
+ float: left;
+ margin-right: 2px;
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ background-image: url('./img/addthis-github.png');
+}
+#addthis-toolbox {
+ position: absolute;
+ right: 15px;
+ top: 40px;
+}
+#content {
+ position: relative;
+ background: #FEFEFE;
+ max-width: 728px;
+ padding: 20px;
+ margin: 30px auto;
+}
+.content-box {
+ padding-left: 10px;
+}
+#content ul {
+ padding-left: 30px;
+}
+/*** footer ***/
+
+footer {
+ position: fixed;
+ left: 0px;
+ bottom: -1px;
+ width: 100%;
+ height: 40px;
+ line-height: 40px;
+ background-color: #1B1B1B;
+ background-image: linear-gradient(to bottom, #222222, #111111);
+ background-repeat: repeat-x;
+ border-color: #252525;
+ box-shadow: 10px 1px 10px rgba(0, 0, 0, 0.5);
+ border-width: 0 0 1px;
+ color: #FFF;
+ font-size: 10px;
+ text-align: left;
+ vertical-align: middle;
+ z-index: 100;
+}
+#footer-icons {
+ position: absolute;
+ right: 10px;
+ top: 0px;
+}
+#footer-icons a {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ width: 17px;
+ height: 17px;
+ margin: 12px 0 0 10px;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+#linkedin-icon { background-image: url('./img/linkedin-icon.png'); width: 18px; }
+#stumbleupon-icon { background-image: url('./img/stumbleupon-icon.png'); }
+#googleplus-icon { background-image: url('./img/googleplus-icon.png'); }
+#youtube-icon { background-image: url('./img/youtube-icon.png'); width: 17px; height: 18px; }
+#facebook-icon { background-image: url('./img/facebook-icon.png'); width: 8px; }
+#twitter-icon { background-image: url('./img/twitter-icon.png'); width: 20px; }
+#github-icon { background-image: url('./img/github-icon.png'); width: 20px; }
+#rss-icon { background-image: url('./img/rss-icon.png'); }
+
+
+/*** media ***/
+
+@media screen and (min-width: 600px) {
+ #header-logo {
+ left: 50px;
+ }
+ #footer-icons {
+ left: auto;
+ right: 50px;
+ }
+ #header-links {
+ display: block !important;
+ width: auto;
+ top: 0px;
+ left: auto;
+ right: 50px;
+ }
+ #header-links a {
+ display: inline;
+ font-size: 10px;
+ margin-left: 20px;
+ text-align: left;
+ background-color: none;
+ border-top: 0px;
+ margin-left: 10px;
+ line-height: inherit;
+ padding: 0px;
+ }
+ #header-menu-button {
+ display: none;
+ }
+ #plugin-name {
+ font-size: 30px;
+ }
+ #addthis-toolbox {
+ top: 44px;
+ }
+}
+
+/*** ads ***/
+
+.websanova-plugins-page-horizontal-responsive { display:block; margin:0 auto; width: 320px; height: 50px; }
+@media(min-width: 520px) { .websanova-plugins-page-horizontal-responsive { width: 468px; height: 60px; } }
+@media(min-width: 800px) { .websanova-plugins-page-horizontal-responsive { width: 728px; height: 90px; } }
+
+.adsblock {
+ position: relative;
+ margin: 0 auto;
+}
+.ads2block {
+ margin-top: 20px;
+}
+.adsblock-mobile-banner {
+ width: 320px;
+ height: 50px;
+}
+.adsblock-banner {
+ display: none;
+ width: 468px;
+ height: 60px;
+}
+.adsblock-leaderboard {
+ display: none;
+ width: 728px;
+ height: 90px;
+}
+@media (min-width:520px) {
+ .adsblock-mobile-banner { display: none; }
+ .adsblock-banner { display: block; }
+}
+@media (min-width:800px) {
+ .adsblock-mobile-banner { display: none; }
+ .adsblock-banner { display: none; }
+ .adsblock-leaderboard { display: block; }
+} \ No newline at end of file
diff --git a/static/js/wpaint/demo/img/facebook-icon.png b/static/js/wpaint/demo/img/facebook-icon.png
new file mode 100644
index 0000000..dfb0f3e
--- /dev/null
+++ b/static/js/wpaint/demo/img/facebook-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/favicon.ico b/static/js/wpaint/demo/img/favicon.ico
new file mode 100644
index 0000000..9bd6326
--- /dev/null
+++ b/static/js/wpaint/demo/img/favicon.ico
Binary files differ
diff --git a/static/js/wpaint/demo/img/forkme_right_darkblue.png b/static/js/wpaint/demo/img/forkme_right_darkblue.png
new file mode 100644
index 0000000..146ef8a
--- /dev/null
+++ b/static/js/wpaint/demo/img/forkme_right_darkblue.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/github-icon.png b/static/js/wpaint/demo/img/github-icon.png
new file mode 100644
index 0000000..a28067c
--- /dev/null
+++ b/static/js/wpaint/demo/img/github-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/googleplus-icon.png b/static/js/wpaint/demo/img/googleplus-icon.png
new file mode 100644
index 0000000..6567f6e
--- /dev/null
+++ b/static/js/wpaint/demo/img/googleplus-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/linkedin-icon.png b/static/js/wpaint/demo/img/linkedin-icon.png
new file mode 100644
index 0000000..a2628e7
--- /dev/null
+++ b/static/js/wpaint/demo/img/linkedin-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/rss-icon.png b/static/js/wpaint/demo/img/rss-icon.png
new file mode 100644
index 0000000..faae141
--- /dev/null
+++ b/static/js/wpaint/demo/img/rss-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/stumbleupon-icon.png b/static/js/wpaint/demo/img/stumbleupon-icon.png
new file mode 100644
index 0000000..5e82ea8
--- /dev/null
+++ b/static/js/wpaint/demo/img/stumbleupon-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/twitter-icon.png b/static/js/wpaint/demo/img/twitter-icon.png
new file mode 100644
index 0000000..499ca0c
--- /dev/null
+++ b/static/js/wpaint/demo/img/twitter-icon.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/websanova-logo-small-full-black.png b/static/js/wpaint/demo/img/websanova-logo-small-full-black.png
new file mode 100644
index 0000000..a5bd0f6
--- /dev/null
+++ b/static/js/wpaint/demo/img/websanova-logo-small-full-black.png
Binary files differ
diff --git a/static/js/wpaint/demo/img/youtube-icon.png b/static/js/wpaint/demo/img/youtube-icon.png
new file mode 100644
index 0000000..e697406
--- /dev/null
+++ b/static/js/wpaint/demo/img/youtube-icon.png
Binary files differ
diff --git a/static/js/wpaint/gruntfile.js b/static/js/wpaint/gruntfile.js
new file mode 100644
index 0000000..4e1eb06
--- /dev/null
+++ b/static/js/wpaint/gruntfile.js
@@ -0,0 +1,90 @@
+module.exports = function(grunt) {
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ jshint: {
+ options: {
+ bitwise: true,
+ camelcase: true,
+ indent: 2,
+ curly: true,
+ eqeqeq: true,
+ immed: true,
+ latedef: true,
+ newcap: true,
+ noarg: true,
+ sub: true,
+ undef: true,
+ unused: true,
+ boss: true,
+ eqnull: true,
+ white: true,
+ validthis: true,
+ quotmark: 'single',
+ globals: {
+ 'window': true,
+ 'jQuery': true,
+ 'document': true,
+ 'Image': true,
+ 'setTimeout': true,
+ 'clearTimeout': true,
+ 'event': true,
+ 'CanvasRenderingContext2D': true
+ }
+ },
+ files: {
+ src: ['./plugins/**/src/wPaint-*.js', './src/*.js']
+ }
+ },
+ uglify: {
+ options: {
+ banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> */'
+ },
+ my_target: {
+ files: {
+ './wPaint.min.js': ['./src/wPaint.js', './src/wPaint.utils.js'],
+ './plugins/main/wPaint.menu.main.min.js': ['./plugins/main/src/wPaint.menu.main.js', './plugins/main/src/fillArea.min.js'],
+ './plugins/text/wPaint.menu.text.min.js': ['./plugins/text/src/wPaint.menu.text.js'],
+ './plugins/shapes/wPaint.menu.main.shapes.min.js': ['./plugins/shapes/src/wPaint.menu.main.shapes.js', './plugins/shapes/src/shapes.min.js'],
+ './plugins/file/wPaint.menu.main.file.min.js': ['./plugins/file/src/wPaint.menu.main.file.js']
+ }
+ }
+ },
+ stylus: {
+ compile: {
+ options: {
+ import: ['nib', '../lib/mixins'],
+ },
+ files: {
+ './wPaint.min.css': './src/wPaint.css'
+ }
+ }
+ },
+ concat: {
+ basic_and_extras: {
+ files: {
+ 'wPaint-min.js': ['./lib/wColorPicker.min.js', './wPaint.min.js'],
+ 'wPaint-min.css': ['./lib/wColorPicker.min.css', './wPaint.min.css'],
+ },
+ }
+ },
+ watch: {
+ files: [
+ './src/wPaint.css',
+ './src/wPaint.js',
+ './plugins/file/src/wPaint.menu.main.js',
+ './plugins/file/src/wPaint.menu.text.js',
+ './plugins/file/src/wPaint.menu.main.shapes.js',
+ './plugins/file/src/wPaint.menu.main.file.js'
+ ],
+ tasks: ['uglify']
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-contrib-stylus');
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ grunt.loadNpmTasks('grunt-contrib-concat');
+
+ grunt.registerTask('default', ['jshint', 'stylus', 'uglify']);
+}; \ No newline at end of file
diff --git a/static/js/wpaint/index.html b/static/js/wpaint/index.html
new file mode 100644
index 0000000..391261e
--- /dev/null
+++ b/static/js/wpaint/index.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width" />
+
+ <title>Websanova :: wPaint</title>
+
+ <!-- filestart -->
+ <link rel="icon" type="image/vnd.microsoft.icon" href="./demo/img/favicon.ico" />
+ <link rel="Stylesheet" type="text/css" href="./demo/demo.css" />
+ <script type="text/javascript" src="./lib/jquery.1.10.2.min.js"></script>
+ <!-- fileend -->
+</head>
+<body>
+ <!-- headstart -->
+ <header>
+ <a id="header-logo" href="http://websanova.com"></a>
+
+ <div id="header-links">
+ <a href="http://websanova.com">Blog</a>
+ <a href="http://websanova.com/plugins">Plugins</a>
+ <a href="http://websanova.com/extensions">Extensions</a>
+ <a href="http://websanova.com/services">Services</a>
+ </div>
+ </header>
+ <!-- headend -->
+
+ <div id="content">
+ <h1 id="plugin-name">wPaint.js</h1>
+
+ <div class="content-box">
+ <!-- jQuery UI -->
+ <script type="text/javascript" src="./lib/jquery.ui.core.1.10.3.min.js"></script>
+ <script type="text/javascript" src="./lib/jquery.ui.widget.1.10.3.min.js"></script>
+ <script type="text/javascript" src="./lib/jquery.ui.mouse.1.10.3.min.js"></script>
+ <script type="text/javascript" src="./lib/jquery.ui.draggable.1.10.3.min.js"></script>
+
+ <!-- wColorPicker -->
+ <link rel="Stylesheet" type="text/css" href="./lib/wColorPicker.min.css" />
+ <script type="text/javascript" src="./lib/wColorPicker.min.js"></script>
+
+ <!-- wPaint -->
+ <link rel="Stylesheet" type="text/css" href="./wPaint.min.css" />
+ <script type="text/javascript" src="./wPaint.min.js"></script>
+ <script type="text/javascript" src="./plugins/main/wPaint.menu.main.min.js"></script>
+ <script type="text/javascript" src="./plugins/text/wPaint.menu.text.min.js"></script>
+ <script type="text/javascript" src="./plugins/shapes/wPaint.menu.main.shapes.min.js"></script>
+ <script type="text/javascript" src="./plugins/file/wPaint.menu.main.file.min.js"></script>
+
+ <div id="wPaint" style="position:relative; width:500px; height:200px; background-color:#7a7a7a; margin:70px auto 20px auto;"></div>
+
+ <center style="margin-bottom: 50px;">
+ <input type="button" value="toggle menu" onclick="console.log($('#wPaint').wPaint('menuOrientation')); $('#wPaint').wPaint('menuOrientation', $('#wPaint').wPaint('menuOrientation') === 'vertical' ? 'horizontal' : 'vertical');"/>
+ </center>
+
+ <center id="wPaint-img"></center>
+
+ <script type="text/javascript">
+ var images = [
+ '/test/uploads/wPaint.png',
+ ];
+
+ function saveImg(image) {
+ var _this = this;
+
+ $.ajax({
+ type: 'POST',
+ url: '/test/upload.php',
+ data: {image: image},
+ success: function (resp) {
+
+ // internal function for displaying status messages in the canvas
+ _this._displayStatus('Image saved successfully');
+
+ // doesn't have to be json, can be anything
+ // returned from server after upload as long
+ // as it contains the path to the image url
+ // or a base64 encoded png, either will work
+ resp = $.parseJSON(resp);
+
+ // update images array / object or whatever
+ // is being used to keep track of the images
+ // can store path or base64 here (but path is better since it's much smaller)
+ images.push(resp.img);
+
+ // do something with the image
+ $('#wPaint-img').attr('src', image);
+ }
+ });
+ }
+
+ function loadImgBg () {
+
+ // internal function for displaying background images modal
+ // where images is an array of images (base64 or url path)
+ // NOTE: that if you can't see the bg image changing it's probably
+ // becasue the foregroud image is not transparent.
+ this._showFileModal('bg', images);
+ }
+
+ function loadImgFg () {
+
+ // internal function for displaying foreground images modal
+ // where images is an array of images (base64 or url path)
+ this._showFileModal('fg', images);
+ }
+
+ // init wPaint
+ $('#wPaint').wPaint({
+ menuOffsetLeft: -35,
+ menuOffsetTop: -50,
+ saveImg: saveImg,
+ loadImgBg: loadImgBg,
+ loadImgFg: loadImgFg
+ });
+ </script>
+ </div>
+ </div>
+
+ <!-- footstart -->
+ <footer>
+ <div id="footer-icons">
+ <!--a id="youtube-icon" href="http://websanova.com/youtube" target="_blank"></a-->
+ <a id="stumbleupon-icon" href="http://websanova.com/stumbleupon" target="_blank"></a>
+ <a id="linkedin-icon" href="http://websanova.com/linkedin" target="_blank"></a>
+ <a id="facebook-icon" href="http://websanova.com/facebook" target="_blank"></a>
+ <a id="googleplus-icon" href="http://websanova.com/googleplus" target="_blank"></a>
+ <a id="twitter-icon" href="http://websanova.com/twitter" target="_blank"></a>
+ <a id="github-icon" href="http://websanova.com/github" target="_blank"></a>
+ <a id="rss-icon" href="http://websanova.com/feed" target="_blank"></a>
+ </div>
+ </footer>
+ <!-- footend -->
+</body>
+</html> \ No newline at end of file
diff --git a/static/js/wpaint/lib/jquery.1.10.2.min.js b/static/js/wpaint/lib/jquery.1.10.2.min.js
new file mode 100644
index 0000000..76d21a4
--- /dev/null
+++ b/static/js/wpaint/lib/jquery.1.10.2.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+//@ sourceMappingURL=jquery-1.10.2.min.map
+*/
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
+}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window); \ No newline at end of file
diff --git a/static/js/wpaint/lib/jquery.ui.core.1.10.3.min.js b/static/js/wpaint/lib/jquery.ui.core.1.10.3.min.js
new file mode 100644
index 0000000..bf1129a
--- /dev/null
+++ b/static/js/wpaint/lib/jquery.ui.core.1.10.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery UI - v1.10.3 - 2013-06-12
+* http://jqueryui.com
+* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */
+(function(e,t){function i(t,i){var a,n,r,o=t.nodeName.toLowerCase();return"area"===o?(a=t.parentNode,n=a.name,t.href&&n&&"map"===a.nodeName.toLowerCase()?(r=e("img[usemap=#"+n+"]")[0],!!r&&s(r)):!1):(/input|select|textarea|button|object/.test(o)?!t.disabled:"a"===o?t.href||i:i)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var a=0,n=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.3",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var s,a,n=e(this[0]);n.length&&n[0]!==document;){if(s=n.css("position"),("absolute"===s||"relative"===s||"fixed"===s)&&(a=parseInt(n.css("zIndex"),10),!isNaN(a)&&0!==a))return a;n=n.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++a)})},removeUniqueId:function(){return this.each(function(){n.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var s=e.attr(t,"tabindex"),a=isNaN(s);return(a||s>=0)&&i(t,!a)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,s){function a(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===s?["Left","Right"]:["Top","Bottom"],r=s.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+s]=function(i){return i===t?o["inner"+s].call(this):this.each(function(){e(this).css(r,a(this,i)+"px")})},e.fn["outer"+s]=function(t,i){return"number"!=typeof t?o["outer"+s].call(this,t):this.each(function(){e(this).css(r,a(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,s){var a,n=e.ui[t].prototype;for(a in s)n.plugins[a]=n.plugins[a]||[],n.plugins[a].push([i,s[a]])},call:function(e,t,i){var s,a=e.plugins[t];if(a&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(s=0;a.length>s;s++)e.options[a[s][0]]&&a[s][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",a=!1;return t[s]>0?!0:(t[s]=1,a=t[s]>0,t[s]=0,a)}})})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js b/static/js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js
new file mode 100644
index 0000000..3b7b149
--- /dev/null
+++ b/static/js/wpaint/lib/jquery.ui.draggable.1.10.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery UI - v1.10.3 - 2013-06-12
+* http://jqueryui.com
+* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */
+(function(e){e.widget("ui.draggable",e.ui.mouse,{version:"1.10.3",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(t){var i=this.options;return this.helper||i.disabled||e(t.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(t),this.handle?(e(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){e("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(e(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(t){var i=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!i.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_mouseDrag:function(t,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(t),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",t,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var i=this,s=!1;return e.ui.ddmanager&&!this.options.dropBehaviour&&(s=e.ui.ddmanager.drop(this,t)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||e.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",t)!==!1&&i._clear()}):this._trigger("stop",t)!==!1&&this._clear(),!1):!1},_mouseUp:function(t){return e("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){return this.options.handle?!!e(t.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(t){var i=this.options,s=e.isFunction(i.helper)?e(i.helper.apply(this.element[0],[t])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(t){"string"==typeof t&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){var t=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&e.ui.ie)&&(t={top:0,left:0}),{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var e=this.element.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,e(window).scrollLeft()+e(window).width()-this.helperProportions.width-this.margins.left,e(window).scrollTop()+(e(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,e(document).width()-this.helperProportions.width-this.margins.left,(e(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=e(n.containment),s=i[0],s&&(t="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(t?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(t?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(t,i){i||(i=this.position);var s="absolute"===t?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(t){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=t.pageX,l=t.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.left<i[0]&&(h=i[0]+this.offset.click.left),t.pageY-this.offset.click.top<i[1]&&(l=i[1]+this.offset.click.top),t.pageX-this.offset.click.left>i[2]&&(h=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(l=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((l-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,l=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((h-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,h=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:l-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:h-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(t,i,s){return s=s||this._uiHash(),e.ui.plugin.call(this,t,[i,s]),"drag"===t&&(this.positionAbs=this._convertPositionTo("absolute")),e.Widget.prototype._trigger.call(this,t,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,i){var s=e(this).data("ui-draggable"),n=s.options,a=e.extend({},i,{item:s.element});s.sortables=[],e(n.connectToSortable).each(function(){var i=e.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",t,a))})},stop:function(t,i){var s=e(this).data("ui-draggable"),n=e.extend({},i,{item:s.element});e.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(t),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",t,n))})},drag:function(t,i){var s=e(this).data("ui-draggable"),n=this;e.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,e.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&e.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=e(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},t.target=this.instance.currentItem[0],this.instance._mouseCapture(t,!0),this.instance._mouseStart(t,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",t),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(t)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",t,this.instance._uiHash(this.instance)),this.instance._mouseStop(t,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",t),s.dropped=!1)})}}),e.ui.plugin.add("draggable","cursor",{start:function(){var t=e("body"),i=e(this).data("ui-draggable").options;t.css("cursor")&&(i._cursor=t.css("cursor")),t.css("cursor",i.cursor)},stop:function(){var t=e(this).data("ui-draggable").options;t._cursor&&e("body").css("cursor",t._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,i){var s=e(i.helper),n=e(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(t,i){var s=e(this).data("ui-draggable").options;s._opacity&&e(i.helper).css("opacity",s._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(){var t=e(this).data("ui-draggable");t.scrollParent[0]!==document&&"HTML"!==t.scrollParent[0].tagName&&(t.overflowOffset=t.scrollParent.offset())},drag:function(t){var i=e(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-t.pageY<s.scrollSensitivity?i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop+s.scrollSpeed:t.pageY-i.overflowOffset.top<s.scrollSensitivity&&(i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop-s.scrollSpeed)),s.axis&&"y"===s.axis||(i.overflowOffset.left+i.scrollParent[0].offsetWidth-t.pageX<s.scrollSensitivity?i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft+s.scrollSpeed:t.pageX-i.overflowOffset.left<s.scrollSensitivity&&(i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft-s.scrollSpeed))):(s.axis&&"x"===s.axis||(t.pageY-e(document).scrollTop()<s.scrollSensitivity?n=e(document).scrollTop(e(document).scrollTop()-s.scrollSpeed):e(window).height()-(t.pageY-e(document).scrollTop())<s.scrollSensitivity&&(n=e(document).scrollTop(e(document).scrollTop()+s.scrollSpeed))),s.axis&&"y"===s.axis||(t.pageX-e(document).scrollLeft()<s.scrollSensitivity?n=e(document).scrollLeft(e(document).scrollLeft()-s.scrollSpeed):e(window).width()-(t.pageX-e(document).scrollLeft())<s.scrollSensitivity&&(n=e(document).scrollLeft(e(document).scrollLeft()+s.scrollSpeed)))),n!==!1&&e.ui.ddmanager&&!s.dropBehaviour&&e.ui.ddmanager.prepareOffsets(i,t)}}),e.ui.plugin.add("draggable","snap",{start:function(){var t=e(this).data("ui-draggable"),i=t.options;t.snapElements=[],e(i.snap.constructor!==String?i.snap.items||":data(ui-draggable)":i.snap).each(function(){var i=e(this),s=i.offset();this!==t.element[0]&&t.snapElements.push({item:this,width:i.outerWidth(),height:i.outerHeight(),top:s.top,left:s.left})})},drag:function(t,i){var s,n,a,o,r,h,l,u,c,d,p=e(this).data("ui-draggable"),f=p.options,m=f.snapTolerance,g=i.offset.left,v=g+p.helperProportions.width,b=i.offset.top,y=b+p.helperProportions.height;for(c=p.snapElements.length-1;c>=0;c--)r=p.snapElements[c].left,h=r+p.snapElements[c].width,l=p.snapElements[c].top,u=l+p.snapElements[c].height,r-m>v||g>h+m||l-m>y||b>u+m||!e.contains(p.snapElements[c].item.ownerDocument,p.snapElements[c].item)?(p.snapElements[c].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=!1):("inner"!==f.snapMode&&(s=m>=Math.abs(l-y),n=m>=Math.abs(u-b),a=m>=Math.abs(r-v),o=m>=Math.abs(h-g),s&&(i.position.top=p._convertPositionTo("relative",{top:l-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:u,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:h}).left-p.margins.left)),d=s||n||a||o,"outer"!==f.snapMode&&(s=m>=Math.abs(l-b),n=m>=Math.abs(u-y),a=m>=Math.abs(r-g),o=m>=Math.abs(h-v),s&&(i.position.top=p._convertPositionTo("relative",{top:l,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:u-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:h-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[c].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=s||n||a||o||d)}}),e.ui.plugin.add("draggable","stack",{start:function(){var t,i=this.data("ui-draggable").options,s=e.makeArray(e(i.stack)).sort(function(t,i){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(i).css("zIndex"),10)||0)});s.length&&(t=parseInt(e(s[0]).css("zIndex"),10)||0,e(s).each(function(i){e(this).css("zIndex",t+i)}),this.css("zIndex",t+s.length))}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,i){var s=e(i.helper),n=e(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(t,i){var s=e(this).data("ui-draggable").options;s._zIndex&&e(i.helper).css("zIndex",s._zIndex)}})})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js b/static/js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js
new file mode 100644
index 0000000..82e3367
--- /dev/null
+++ b/static/js/wpaint/lib/jquery.ui.mouse.1.10.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery UI - v1.10.3 - 2013-06-12
+* http://jqueryui.com
+* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */
+(function(e){var t=!1;e(document).mouseup(function(){t=!1}),e.widget("ui.mouse",{version:"1.10.3",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(i){return!0===e.data(i.target,t.widgetName+".preventClickEvent")?(e.removeData(i.target,t.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!t){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?e(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===e.data(i.target,this.widgetName+".preventClickEvent")&&e.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return s._mouseMove(e)},this._mouseUpDelegate=function(e){return s._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),t=!0,!0)):!0}},_mouseMove:function(t){return e.ui.ie&&(!document.documentMode||9>document.documentMode)&&!t.button?this._mouseUp(t):this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/lib/jquery.ui.widget.1.10.3.min.js b/static/js/wpaint/lib/jquery.ui.widget.1.10.3.min.js
new file mode 100644
index 0000000..5f7a7c1
--- /dev/null
+++ b/static/js/wpaint/lib/jquery.ui.widget.1.10.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery UI - v1.10.3 - 2013-06-12
+* http://jqueryui.com
+* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */
+(function(e,t){var i=0,s=Array.prototype.slice,n=e.cleanData;e.cleanData=function(t){for(var i,s=0;null!=(i=t[s]);s++)try{e(i).triggerHandler("remove")}catch(a){}n(t)},e.widget=function(i,s,n){var a,r,o,h,l={},u=i.split(".")[0];i=i.split(".")[1],a=u+"-"+i,n||(n=s,s=e.Widget),e.expr[":"][a.toLowerCase()]=function(t){return!!e.data(t,a)},e[u]=e[u]||{},r=e[u][i],o=e[u][i]=function(e,i){return this._createWidget?(arguments.length&&this._createWidget(e,i),t):new o(e,i)},e.extend(o,r,{version:n.version,_proto:e.extend({},n),_childConstructors:[]}),h=new s,h.options=e.widget.extend({},h.options),e.each(n,function(i,n){return e.isFunction(n)?(l[i]=function(){var e=function(){return s.prototype[i].apply(this,arguments)},t=function(e){return s.prototype[i].apply(this,e)};return function(){var i,s=this._super,a=this._superApply;return this._super=e,this._superApply=t,i=n.apply(this,arguments),this._super=s,this._superApply=a,i}}(),t):(l[i]=n,t)}),o.prototype=e.widget.extend(h,{widgetEventPrefix:r?h.widgetEventPrefix:i},l,{constructor:o,namespace:u,widgetName:i,widgetFullName:a}),r?(e.each(r._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete r._childConstructors):s._childConstructors.push(o),e.widget.bridge(i,o)},e.widget.extend=function(i){for(var n,a,r=s.call(arguments,1),o=0,h=r.length;h>o;o++)for(n in r[o])a=r[o][n],r[o].hasOwnProperty(n)&&a!==t&&(i[n]=e.isPlainObject(a)?e.isPlainObject(i[n])?e.widget.extend({},i[n],a):e.widget.extend({},a):a);return i},e.widget.bridge=function(i,n){var a=n.prototype.widgetFullName||i;e.fn[i]=function(r){var o="string"==typeof r,h=s.call(arguments,1),l=this;return r=!o&&h.length?e.widget.extend.apply(null,[r].concat(h)):r,o?this.each(function(){var s,n=e.data(this,a);return n?e.isFunction(n[r])&&"_"!==r.charAt(0)?(s=n[r].apply(n,h),s!==n&&s!==t?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):t):e.error("no such method '"+r+"' for "+i+" widget instance"):e.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+r+"'")}):this.each(function(){var t=e.data(this,a);t?t.option(r||{})._init():e.data(this,a,new n(r,this))}),l}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(t,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(i,s){var n,a,r,o=i;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof i)if(o={},n=i.split("."),i=n.shift(),n.length){for(a=o[i]=e.widget.extend({},this.options[i]),r=0;n.length-1>r;r++)a[n[r]]=a[n[r]]||{},a=a[n[r]];if(i=n.pop(),s===t)return a[i]===t?null:a[i];a[i]=s}else{if(s===t)return this.options[i]===t?null:this.options[i];o[i]=s}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var a,r=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=a=e(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,a=this.widget()),e.each(n,function(n,o){function h(){return i||r.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?r[o]:o).apply(r,arguments):t}"string"!=typeof o&&(h.guid=o.guid=o.guid||h.guid||e.guid++);var l=n.match(/^(\w+)\s*(.*)$/),u=l[1]+r.eventNamespace,c=l[2];c?a.delegate(c,u,h):s.bind(u,h)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,r=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(r)&&r.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var r,o=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),r=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),r&&e.effects&&e.effects.effect[o]?s[t](n):o!==t&&s[o]?s[o](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}})})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/lib/mixins.styl b/static/js/wpaint/lib/mixins.styl
new file mode 100644
index 0000000..b92debe
--- /dev/null
+++ b/static/js/wpaint/lib/mixins.styl
@@ -0,0 +1,7 @@
+display(type)
+ if type == 'inline-block'
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ else
+ display: type; \ No newline at end of file
diff --git a/static/js/wpaint/lib/wColorPicker.min.css b/static/js/wpaint/lib/wColorPicker.min.css
new file mode 100644
index 0000000..9c39b88
--- /dev/null
+++ b/static/js/wpaint/lib/wColorPicker.min.css
@@ -0,0 +1,42 @@
+.wColorPicker{position:relative;display:inline-block;*display:inline;zoom:1;line-height:0;font-size:0;cursor:default}
+.wColorPicker-bg{position:absolute;left:0;top:0;width:100%;height:100%}
+.wColorPicker-holder{position:relative;display:inline-block;*display:inline;zoom:1;overflow:hidden}
+.wColorPicker-palettes-holder{position:relative;display:inline-block;*display:inline;zoom:1}
+.wColorPicker-zindex{z-index:1000}
+.wColorPicker-palette{position:relative;display:inline-block;*display:inline;zoom:1;margin:0 10px 10px 0}
+.wColorPicker-palette-none,.wColorPicker-palette-simple{margin-left:10px}
+.wColorPicker-palette-color{display:inline-block;*display:inline;zoom:1;vertical-align:middle;width:10px;height:10px;border-right:solid 1px;border-bottom:solid 1px;cursor:pointer}
+.wColorPicker-palette-color-left{border-left:solid 1px}
+.wColorPicker-palette-color-top{border-top:solid 1px}
+.wColorPicker-color-target{position:relative;display:inline-block;*display:inline;zoom:1;vertical-align:middle;width:40px;height:16px;border:solid 1px}
+.wColorPicker-custom-input{position:relative;display:inline-block;*display:inline;zoom:1;vertical-align:middle;width:70px;height:16px;line-height:16px;font-size:9px;font-family:verdana;padding:0 2px;border:solid 1px}
+.wColorPicker-color-target,.wColorPicker-custom-input{margin:10px 10px 10px 0}
+.wColorPicker-dropper{position:relative;display:inline-block;*display:inline;zoom:1;vertical-align:middle;width:16px;height:16px;border:solid 1px;cursor:pointer}
+.wColorPicker-button{position:relative;-webkit-border-radius:5px;border-radius:5px;border:solid #cacaca 1px;padding:1px}
+.wColorPicker-button-color{position:relative;-webkit-border-radius:5px;border-radius:5px}
+.wColorPicker-holder{border:solid 1px}
+.wColorPicker-bg,.wColorPicker-holder{-webkit-border-radius:5px;border-radius:5px}
+.wColorPicker-bg{-webkit-box-shadow:inset 2px 2px 3px #fff,1px 1px 2px #555;box-shadow:inset 2px 2px 3px #fff,1px 1px 2px #555}
+.wColorPicker-custom-input{opacity:.5;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";filter:alpha(opacity=50)}
+.wColorPicker-dropper{opacity:.6;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=60)";filter:alpha(opacity=60);-webkit-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 1px 1px 2px #fff,0 0 1px #777;box-shadow:inset 1px 1px 2px #fff,0 0 1px #777;background-color:#cacaca;background:no-repeat center center url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwOC8wOS8xM9AohnYAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAA4ElEQVQokYXSMUpDARCE4e/FBCEggoVH0BtY2bsiiIWIipVHsLaOYCcWVtpYSBBEsNnO9qGFR/AKVhISNRa+SJDkudWw/DMwyxbD4dB/ExEnWMFBUWeIiAVs4RxtPDdr4DUcY7Va9fA01YAzLFd6gKPMvGhMSd/EKR6r1QsuofkHLLCDr8y8iohPLKKTmX34LR0RLeyij3uso4GHzByMQhtj8B56mdmt4HZm3o3DMFOWZYGogJuI2MZ8Zl5P6tfEHFroRsQ+ZjERHhkOK72Bd9xm5ked4RVLfk74lpm1v/INodZOOpic3qMAAAAASUVORK5CYII=")}
+.wColorPicker-theme-classic .wColorPicker-dropper,.wColorPicker-theme-classic .wColorPicker-custom-input,.wColorPicker-theme-classic .wColorPicker-color-target,.wColorPicker-theme-classic .wColorPicker-palette-color{border-color:#3a3a3a}
+.wColorPicker-theme-classic .wColorPicker-holder{border-color:#bababa}
+.wColorPicker-theme-classic .wColorPicker-bg{background-color:#aaa}
+.wColorPicker-theme-black .wColorPicker-dropper,.wColorPicker-theme-black .wColorPicker-color-target,.wColorPicker-theme-black .wColorPicker-color-input,.wColorPicker-theme-black .wColorPicker-palette-color{border-color:#3f3f3f}
+.wColorPicker-theme-black .wColorPicker-holder{border-color:#7f7f7f}
+.wColorPicker-theme-black .wColorPicker-bg{background-color:#363636}
+.wColorPicker-theme-blue .wColorPicker-dropper,.wColorPicker-theme-blue .wColorPicker-color-target,.wColorPicker-theme-blue .wColorPicker-color-input,.wColorPicker-theme-blue .wColorPicker-palette-color{border-color:#002f4d}
+.wColorPicker-theme-blue .wColorPicker-holder{border-color:#49afcd}
+.wColorPicker-theme-blue .wColorPicker-bg{background-color:#2f96b4}
+.wColorPicker-theme-red .wColorPicker-dropper,.wColorPicker-theme-red .wColorPicker-color-target,.wColorPicker-theme-red .wColorPicker-color-input,.wColorPicker-theme-red .wColorPicker-palette-color{border-color:#8a0f09}
+.wColorPicker-theme-red .wColorPicker-holder{border-color:#da4f49}
+.wColorPicker-theme-red .wColorPicker-bg{background-color:#bd362f}
+.wColorPicker-theme-green .wColorPicker-dropper,.wColorPicker-theme-green .wColorPicker-color-target,.wColorPicker-theme-green .wColorPicker-color-input,.wColorPicker-theme-green .wColorPicker-palette-color{border-color:#0b670b}
+.wColorPicker-theme-green .wColorPicker-holder{border-color:#5bb75b}
+.wColorPicker-theme-green .wColorPicker-bg{background-color:#51a351}
+.wColorPicker-theme-orange .wColorPicker-dropper,.wColorPicker-theme-orange .wColorPicker-color-target,.wColorPicker-theme-orange .wColorPicker-color-input,.wColorPicker-theme-orange .wColorPicker-palette-color{border-color:#9a5700}
+.wColorPicker-theme-orange .wColorPicker-holder{border-color:#faa732}
+.wColorPicker-theme-orange .wColorPicker-bg{background-color:#f89406}
+.wColorPicker-palette-color.active{border-color:#f00}
+.wColorPicker-palette-color.active-right{border-right-color:#f00}
+.wColorPicker-palette-color.active-bottom{border-bottom-color:#f00}
diff --git a/static/js/wpaint/lib/wColorPicker.min.js b/static/js/wpaint/lib/wColorPicker.min.js
new file mode 100644
index 0000000..cb515e6
--- /dev/null
+++ b/static/js/wpaint/lib/wColorPicker.min.js
@@ -0,0 +1,2 @@
+/*! rgbHex - v1.1.2 - 2013-09-27 */window.rgbHex=function(){function a(a){return!isNaN(parseFloat(a))&&isFinite(a)}function b(a){return a.replace(/^\s+|\s+$/g,"")}function c(c){return c=b(c),a(c)&&c>=0&&255>=c}function d(a){return/^[0-9a-f]{3}$|^[0-9a-f]{6}$/i.test(b(a))}function e(a){return a=parseInt(a,10).toString(16),1===a.length?"0"+a:a}function f(a){return parseInt(a,16).toString()}function g(b){return b=b.split(","),(3===b.length||4===b.length)&&c(b[0])&&c(b[1])&&c(b[2])?4!==b.length||a(b[3])?"#"+e(b[0]).toUpperCase()+e(b[1]).toUpperCase()+e(b[2]).toUpperCase():null:null}function h(a){return d(a)?(3===a.length&&(a=a.charAt(0)+a.charAt(0)+a.charAt(1)+a.charAt(1)+a.charAt(2)+a.charAt(2)),"rgb("+f(a.substr(0,2))+","+f(a.substr(2,2))+","+f(a.substr(4,2))+")"):void 0}function i(a){return a.replace(/\s/g,"")}return function(a){if(!a)return null;var c=null,d=/^rgba?\((.*)\);?$/,e=/^#/;return a=b(a.toString()),"transparent"===a||"rgba(0,0,0,0)"===i(a)?"transparent":d.test(a)?g(a.match(d)[1]):e.test(a)?h(a.split("#").reverse()[0]):(c=a.split(","),1===c.length?h(a):3===c.length||4===c.length?g(a):void 0)}}(),jQuery&&jQuery.extend({rgbHex:function(a){return window.rgbHex(a)}});
+/*! wColorPicker - v2.1.7 - 2013-09-27 */!function(a){function b(b,c){this.$el=a(b),this.options=c,this.init=!1,this.generate()}b.prototype={generate:function(){return this.$colorPicker||(this.$noneColorPalette=this._createPalette("none",[["transparent"]]),this.$simpleColorPalette=this._createPalette("simple",a.fn.wColorPicker.simpleColors),this.$mixedColorPalette=this._createPalette("mixed",a.fn.wColorPicker.mixedColors),this.$colorTarget=a('<div class="wColorPicker-color-target"></div>'),this.$customInput=a('<input type="text" class="wColorPicker-custom-input"/>').keyup(a.proxy(this._customInputKeyup,this)),this.options.dropperButton&&(this.$dropperButton=this._createDropperButton()),this.$colorPickerPalettesHolder=a('<div class="wColorPicker-palettes-holder"></div>').append(this.$noneColorPalette).append(this.$colorTarget).append(this.$customInput).append(this.$dropperButton).append("<br/>").append(this.$simpleColorPalette).append(this.$mixedColorPalette),this.$colorPickerHolder=a('<div class="wColorPicker-holder"></div>').append(this.$colorPickerPalettesHolder),this.$colorPickerBg=a('<div class="wColorPicker-bg"></div>'),this.$colorPicker=a('<div class="wColorPicker" title=""></div>').mouseenter(function(a){a.stopPropagation()}).bind("mouseenter mousemove click",function(a){a.stopPropagation()}).append(this.$colorPickerBg).append(this.$colorPickerHolder),this.setOpacity(this.options.opacity),this.setTheme(this.options.theme),this.setColor(this.options.color),a("body").append(this.$colorPicker),this.width=this.$colorPickerPalettesHolder.width(),this.height=this.$colorPickerPalettesHolder.height(),this.$colorPickerPalettesHolder.width(this.width),this.$colorPickerPalettesHolder.height(this.height),this.$el.append(this.$colorPicker),this.setMode(this.options.mode),this.setPosition(this.options.position)),this.init=!0,this},setTheme:function(a){this.$colorPicker.attr("class",this.$colorPicker.attr("class").replace(/wColorPicker-theme-.+\s|wColorPicker-theme-.+$/,"")),this.$colorPicker.addClass("wColorPicker-theme-"+a)},setOpacity:function(a){this.$colorPickerBg.css("opacity",a)},setColor:function(a){return window.rgbHex(a)?(this.options.color=a,this.$colorTarget.css("backgroundColor",a),this.$customInput.val(a),this.init&&this.options.onSelect&&this.options.onSelect.apply(this,[a]),void 0):!0},setMode:function(b){var c=this,d=function(){c._toggleEffect("show")},e=function(){c._toggleEffect("hide")};if("flat"===b?this.$colorPicker.removeClass("wColorPicker-zindex").css({position:"relative",display:""}):this.$colorPicker.addClass("wColorPicker-zindex").css({position:"absolute"}).hide(),this.$el.find("wColorPicker-button").remove(),this.$el.unbind("mouseenter",d).unbind("mouseleave",e),a(document).unbind("click",e),"flat"!==b){var f=null,g=null,h=function(a){a.stopPropagation(),c.options.generateButton&&g.css("backgroundColor",c.options.color),c._toggleEffect()};this.options.generateButton&&(f=a('<div class="wColorPicker-button"></div>'),g=a('<div class="wColorPicker-button-color"></div>').css("backgroundColor",this.options.color),this.$el.append(f),f.append(g.height(this.$el.height()-f.outerHeight(!0)))),this.$noneColorPalette.bind("click",h),this.$simpleColorPalette.bind("click",h),this.$mixedColorPalette.bind("click",h)}"click"===b?(this.$el.click(function(a){c._toggleEffect(),a.stopPropagation()}),this.$colorPicker.click(function(a){a.stopPropagation()}),a(document).bind("click",e)):"hover"===b&&this.$el.bind("mouseenter",d).bind("mouseleave",e)},setEffect:function(a){return"flat"===this.options.mode?!0:(this.$colorPicker.css("opacity",1),this.$colorPickerHolder.width(this.width).height(this.height),"fade"===a?this.$colorPicker.css("opacity",0):"slide"===a&&this.$colorPickerHolder.width("x"===this.positionAxis?0:this.width).height("y"===this.positionAxis?0:this.height),void 0)},setPosition:function(a){if("flat"===this.options.mode)return!0;var b=this.$el.outerWidth(),c=this.$el.outerHeight(),d=this.$el.outerWidth()/2-this.$colorPicker.outerWidth()/2,e=this.$el.outerHeight()/2-this.$colorPicker.outerHeight()/2,f={left:"",right:"",top:"",bottom:""},g=this.options.position.charAt(0);switch("t"===g||"b"===g?this.positionAxis="y":("l"===g||"r"===g)&&(this.positionAxis="x"),a){case"tl":f.left=0,f.bottom=c;break;case"tc":f.left=d,f.bottom=c;break;case"tr":f.right=0,f.bottom=c;break;case"rt":f.left=b,f.top=0;break;case"rm":f.left=b,f.top=e;break;case"rb":f.left=b,f.bottom=0;break;case"br":f.right=0,f.top=c;break;case"bc":f.left=d,f.top=c;break;case"bl":f.left=0,f.top=c;break;case"lb":f.right=b,f.bottom=0;break;case"lm":f.right=b,f.top=e;break;case"lt":f.right=b,f.top=0}this.$colorPicker.css(f),this.setEffect(this.options.effect)},_createPalette:function(b,c){var d=0,e=0,f=0,g=0,h=null,i=a('<div class="wColorPicker-palette wColorPicker-palette-'+b+'"></div>');for(d=0,e=c.length;e>d;d++){for(f=0,g=c[d].length;g>f;f++)h=this._createColor(f,c[d][f]),0===d&&h.addClass("wColorPicker-palette-color-top"),0===f&&h.addClass("wColorPicker-palette-color-left"),i.append(h);e>d&&i.append("<br/>")}return i},_createColor:function(b,c){var d=this;return a('<div class="wColorPicker-palette-color"></div>').attr("id","wColorPicker-palette-color-"+b).addClass("wColorPicker-palette-color-"+b).css("backgroundColor",c).hover(function(){d._colorMouseenter(a(this))},function(){d._colorMouseleave(a(this))}).click(function(){d.setColor(window.rgbHex(a(this).css("backgroundColor")))})},_createDropperButton:function(){return a('<div class="wColorPicker-dropper"></div>').click(a.proxy(this.options.onDropper,this))},_customInputKeyup:function(b){var c=a(b.target).val();window.rgbHex(c)&&(this.$colorTarget.css("backgroundColor",c),13===b.keyCode&&this.setColor(c))},_colorMouseenter:function(a){var b=window.rgbHex(a.css("backgroundColor"));a.addClass("active").prev().addClass("active-right"),a.prevAll("."+a.attr("id")+":first").addClass("active-bottom"),this.$colorTarget.css("backgroundColor",b),this.$customInput.val(b),this.options.onMouseover&&this.options.onMouseover.apply(this,[b])},_colorMouseleave:function(a){a.removeClass("active").prev().removeClass("active-right"),a.prevAll("."+a.attr("id")+":first").removeClass("active-bottom"),this.$colorTarget.css("backgroundColor",this.options.color),this.$customInput.val(this.options.color),this.options.onMouseout&&this.options.onMouseout.apply(this,[this.options.color])},_toggleEffect:function(a){var b=this.$colorPicker.hasClass("wColorPicker-visible");(!a||"show"===a&&b===!1||"hide"===a&&b===!0)&&(b||this.setPosition(this.options.position),this["_"+this.options.effect+"Effect"+(b?"Hide":"Show")](),this.$colorPicker.toggleClass("wColorPicker-visible"))},_noneEffectShow:function(){this.$colorPicker.css("display","inline-block")},_noneEffectHide:function(){this.$colorPicker.hide()},_fadeEffectShow:function(){this.$colorPicker.stop(!0,!1).css({display:"inline-block"}).animate({opacity:1},this.options.showSpeed)},_fadeEffectHide:function(){this.$colorPicker.stop(!0,!1).animate({opacity:0},this.options.hideSpeed,a.proxy(function(){this.$colorPicker.hide()},this))},_slideEffectShow:function(){var a="y"===this.positionAxis?{height:this.height}:{width:this.width};this.$colorPicker.css("display","inline-block"),this.$colorPickerHolder.stop(!0,!1).animate(a,this.options.showSpeed)},_slideEffectHide:function(){var b="y"===this.positionAxis?{height:0}:{width:0};this.$colorPickerHolder.stop(!0,!1).animate(b,this.options.hideSpeed,a.proxy(function(){this.$colorPicker.hide()},this))}},a.fn.wColorPicker=function(c,d){function e(d){var e,f=a.data(d,"wColorPicker");return f||(e=a.extend({},a.fn.wColorPicker.defaults,c),e.color=window.rgbHex(e.color)?e.color:"transparent",f=new b(d,e),a.data(d,"wColorPicker",f)),f}if("string"==typeof c){var f=[],g=null,h=null,i=null;return h=this.each(function(){g=a(this).data("wColorPicker"),g&&(i=(d?"set":"get")+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase(),g[c]?f.push(g[c].apply(g,[d])):d?(g[i]&&g[i].apply(g,[d]),g.options[c]&&(g.options[c]=d)):g[i]?f.push(g[i].apply(g,[d])):g.options[c]?f.push(g.options[c]):f.push(null))}),1===f.length?f[0]:f.length>0?f:h}return this.each(function(){e(this)})},a.fn.wColorPicker.defaults={theme:"classic",opacity:.9,color:"#FF0000",mode:"flat",position:"bl",generateButton:!0,dropperButton:!1,effect:"slide",showSpeed:500,hideSpeed:500,onMouseover:null,onMouseout:null,onSelect:null,onDropper:null},a.fn.wColorPicker.mixedColors=[["#000000","#003300","#006600","#009900","#00CC00","#00FF00","#330000","#333300","#336600","#339900","#33CC00","#33FF00","#660000","#663300","#666600","#669900","#66CC00","#66FF00"],["#000033","#003333","#006633","#009933","#00CC33","#00FF33","#330033","#333333","#336633","#339933","#33CC33","#33FF33","#660033","#663333","#666633","#669933","#66CC33","#66FF33"],["#000066","#003366","#006666","#009966","#00CC66","#00FF66","#330066","#333366","#336666","#339966","#33CC66","#33FF66","#660066","#663366","#666666","#669966","#66CC66","#66FF66"],["#000099","#003399","#006699","#009999","#00CC99","#00FF99","#330099","#333399","#336699","#339999","#33CC99","#33FF99","#660099","#663399","#666699","#669999","#66CC99","#66FF99"],["#0000CC","#0033CC","#0066CC","#0099CC","#00CCCC","#00FFCC","#3300CC","#3333CC","#3366CC","#3399CC","#33CCCC","#33FFCC","#6600CC","#6633CC","#6666CC","#6699CC","#66CCCC","#66FFCC"],["#0000FF","#0033FF","#0066FF","#0099FF","#00CCFF","#00FFFF","#3300FF","#3333FF","#3366FF","#3399FF","#33CCFF","#33FFFF","#6600FF","#6633FF","#6666FF","#6699FF","#66CCFF","#66FFFF"],["#990000","#993300","#996600","#999900","#99CC00","#99FF00","#CC0000","#CC3300","#CC6600","#CC9900","#CCCC00","#CCFF00","#FF0000","#FF3300","#FF6600","#FF9900","#FFCC00","#FFFF00"],["#990033","#993333","#996633","#999933","#99CC33","#99FF33","#CC0033","#CC3333","#CC6633","#CC9933","#CCCC33","#CCFF33","#FF0033","#FF3333","#FF6633","#FF9933","#FFCC33","#FFFF33"],["#990066","#993366","#996666","#999966","#99CC66","#99FF66","#CC0066","#CC3366","#CC6666","#CC9966","#CCCC66","#CCFF66","#FF0066","#FF3366","#FF6666","#FF9966","#FFCC66","#FFFF66"],["#990099","#993399","#996699","#999999","#99CC99","#99FF99","#CC0099","#CC3399","#CC6699","#CC9999","#CCCC99","#CCFF99","#FF0099","#FF3399","#FF6699","#FF9999","#FFCC99","#FFFF99"],["#9900CC","#9933CC","#9966CC","#9999CC","#99CCCC","#99FFCC","#CC00CC","#CC33CC","#CC66CC","#CC99CC","#CCCCCC","#CCFFCC","#FF00CC","#FF33CC","#FF66CC","#FF99CC","#FFCCCC","#FFFFCC"],["#9900FF","#9933FF","#9966FF","#9999FF","#99CCFF","#99FFFF","#CC00FF","#CC33FF","#CC66FF","#CC99FF","#CCCCFF","#CCFFFF","#FF00FF","#FF33FF","#FF66FF","#FF99FF","#FFCCFF","#FFFFFF"]],a.fn.wColorPicker.simpleColors=[["#000000"],["#333333"],["#666666"],["#999999"],["#CCCCCC"],["#FFFFFF"],["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#00FFFF"],["#FF00FF"]]}(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/package.json b/static/js/wpaint/package.json
new file mode 100644
index 0000000..49ea26f
--- /dev/null
+++ b/static/js/wpaint/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "wPaint",
+ "title": "wPaint jQuery Paint Plugin",
+ "version": "2.5.0",
+ "description": "A jQuery paint plugin for a simple drawing surface that you can easily pop into your pages, similar to the basic windows paint program.",
+ "main": "wPaint.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/websanova/wPaint"
+ },
+ "author": {
+ "name": "Websanova",
+ "email": "rob@websanova.com",
+ "url": "http://websanova.com"
+ },
+ "homepage" : "http://wpaint.websanova.com",
+ "license": "MIT, GPL",
+ "dependencies": {
+ "grunt-contrib-uglify": "",
+ "grunt-contrib-jshint": "",
+ "grunt-contrib-stylus": "",
+ "grunt-contrib-concat": "",
+ "grunt-contrib-watch": ""
+ }
+} \ No newline at end of file
diff --git a/static/js/wpaint/plugins/file/img/icons-menu-main-file.png b/static/js/wpaint/plugins/file/img/icons-menu-main-file.png
new file mode 100644
index 0000000..cda4bde
--- /dev/null
+++ b/static/js/wpaint/plugins/file/img/icons-menu-main-file.png
Binary files differ
diff --git a/static/js/wpaint/plugins/file/src/wPaint.menu.main.file.js b/static/js/wpaint/plugins/file/src/wPaint.menu.main.file.js
new file mode 100644
index 0000000..15cfa46
--- /dev/null
+++ b/static/js/wpaint/plugins/file/src/wPaint.menu.main.file.js
@@ -0,0 +1,75 @@
+(function ($) {
+ var img = 'plugins/file/img/icons-menu-main-file.png';
+
+ // extend menu
+ $.extend(true, $.fn.wPaint.menus.main.items, {
+ save: {
+ icon: 'generic',
+ title: 'Save Image',
+ img: img,
+ index: 0,
+ callback: function () {
+ this.options.saveImg.apply(this, [this.getImage()]);
+ }
+ },
+ loadBg: {
+ icon: 'generic',
+ group: 'loadImg',
+ title: 'Load Image to Foreground',
+ img: img,
+ index: 2,
+ callback: function () {
+ this.options.loadImgFg.apply(this, []);
+ }
+ },
+ loadFg: {
+ icon: 'generic',
+ group: 'loadImg',
+ title: 'Load Image to Background',
+ img: img,
+ index: 1,
+ callback: function () {
+ this.options.loadImgBg.apply(this, []);
+ }
+ }
+ });
+
+ // extend defaults
+ $.extend($.fn.wPaint.defaults, {
+ saveImg: null, // callback triggerd on image save
+ loadImgFg: null, // callback triggered on image fg
+ loadImgBg: null // callback triggerd on image bg
+ });
+
+ // extend functions
+ $.fn.wPaint.extend({
+ _showFileModal: function (type, images) {
+ var _this = this,
+ $content = $('<div></div>'),
+ $img = null;
+
+ function appendContent(type, image) {
+ function imgClick(e) {
+
+ // just in case to not draw on canvas
+ e.stopPropagation();
+ if (type === 'fg') { _this.setImage(image); }
+ else if (type === 'bg') { _this.setBg(image, null, null, true); }
+ }
+
+ $img.on('click', imgClick);
+ }
+
+ for (var i = 0, ii = images.length; i < ii; i++) {
+ $img = $('<img class="wPaint-modal-img"/>').attr('src', images[i]);
+ $img = $('<div class="wPaint-modal-img-holder"></div>').append($img);
+
+ (appendContent)(type, images[i]);
+
+ $content.append($img);
+ }
+
+ this._showModal($content);
+ }
+ });
+})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/file/wPaint.menu.main.file.min.js b/static/js/wpaint/plugins/file/wPaint.menu.main.file.min.js
new file mode 100644
index 0000000..a2631b7
--- /dev/null
+++ b/static/js/wpaint/plugins/file/wPaint.menu.main.file.min.js
@@ -0,0 +1 @@
+/*! wPaint - v2.5.0 - 2014-03-01 */!function(a){var b="plugins/file/img/icons-menu-main-file.png";a.extend(!0,a.fn.wPaint.menus.main.items,{save:{icon:"generic",title:"Save Image",img:b,index:0,callback:function(){this.options.saveImg.apply(this,[this.getImage()])}},loadBg:{icon:"generic",group:"loadImg",title:"Load Image to Foreground",img:b,index:2,callback:function(){this.options.loadImgFg.apply(this,[])}},loadFg:{icon:"generic",group:"loadImg",title:"Load Image to Background",img:b,index:1,callback:function(){this.options.loadImgBg.apply(this,[])}}}),a.extend(a.fn.wPaint.defaults,{saveImg:null,loadImgFg:null,loadImgBg:null}),a.fn.wPaint.extend({_showFileModal:function(b,c){function d(a,b){function c(c){c.stopPropagation(),"fg"===a?e.setImage(b):"bg"===a&&e.setBg(b,null,null,!0)}g.on("click",c)}for(var e=this,f=a("<div></div>"),g=null,h=0,i=c.length;i>h;h++)g=a('<img class="wPaint-modal-img"/>').attr("src",c[h]),g=a('<div class="wPaint-modal-img-holder"></div>').append(g),d(b,c[h]),f.append(g);this._showModal(f)}})}(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/main/img/cursor-bucket.png b/static/js/wpaint/plugins/main/img/cursor-bucket.png
new file mode 100644
index 0000000..b6757ad
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-bucket.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-crosshair.png b/static/js/wpaint/plugins/main/img/cursor-crosshair.png
new file mode 100644
index 0000000..362e069
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-crosshair.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-dropper.png b/static/js/wpaint/plugins/main/img/cursor-dropper.png
new file mode 100644
index 0000000..e8396d1
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-dropper.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser1.png b/static/js/wpaint/plugins/main/img/cursor-eraser1.png
new file mode 100644
index 0000000..1a25ec5
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser1.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser10.png b/static/js/wpaint/plugins/main/img/cursor-eraser10.png
new file mode 100644
index 0000000..5f5d83f
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser10.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser2.png b/static/js/wpaint/plugins/main/img/cursor-eraser2.png
new file mode 100644
index 0000000..d15f875
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser2.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser3.png b/static/js/wpaint/plugins/main/img/cursor-eraser3.png
new file mode 100644
index 0000000..c81814d
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser3.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser4.png b/static/js/wpaint/plugins/main/img/cursor-eraser4.png
new file mode 100644
index 0000000..c512bfc
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser4.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser5.png b/static/js/wpaint/plugins/main/img/cursor-eraser5.png
new file mode 100644
index 0000000..fff0873
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser5.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser6.png b/static/js/wpaint/plugins/main/img/cursor-eraser6.png
new file mode 100644
index 0000000..d413f05
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser6.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser7.png b/static/js/wpaint/plugins/main/img/cursor-eraser7.png
new file mode 100644
index 0000000..52876a5
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser7.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser8.png b/static/js/wpaint/plugins/main/img/cursor-eraser8.png
new file mode 100644
index 0000000..6c1577b
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser8.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-eraser9.png b/static/js/wpaint/plugins/main/img/cursor-eraser9.png
new file mode 100644
index 0000000..4441a3e
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-eraser9.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/cursor-pencil.png b/static/js/wpaint/plugins/main/img/cursor-pencil.png
new file mode 100644
index 0000000..d54322d
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/cursor-pencil.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/icon-group-arrow.png b/static/js/wpaint/plugins/main/img/icon-group-arrow.png
new file mode 100644
index 0000000..ff501ac
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/icon-group-arrow.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/img/icons-menu-main.png b/static/js/wpaint/plugins/main/img/icons-menu-main.png
new file mode 100644
index 0000000..4b1a90b
--- /dev/null
+++ b/static/js/wpaint/plugins/main/img/icons-menu-main.png
Binary files differ
diff --git a/static/js/wpaint/plugins/main/src/fillArea.min.js b/static/js/wpaint/plugins/main/src/fillArea.min.js
new file mode 100644
index 0000000..08aa950
--- /dev/null
+++ b/static/js/wpaint/plugins/main/src/fillArea.min.js
@@ -0,0 +1 @@
+!function(){window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.fillArea=function(a,b,c){function d(a){return{r:p[a],g:p[a+1],b:p[a+2],a:p[a+3]}}function e(a){p[a]=c.r,p[a+1]=c.g,p[a+2]=c.b,p[a+3]=c.a}function f(a){return g.r===a.r&&g.g===a.g&&g.b===a.b&&g.a===a.a}if(!a||!b||!c)return!0;var g,h,i,j,k,l,m=this.canvas.width,n=this.canvas.height,o=this.getImageData(0,0,m,n),p=o.data,q=[[a,b]];if(g=d(4*(b*m+a)),l=this.canvas.style.color,this.canvas.style.color=c,c=this.canvas.style.color.match(/^rgba?\((.*)\);?$/)[1].split(","),this.canvas.style.color=l,c={r:parseInt(c[0],10),g:parseInt(c[1],10),b:parseInt(c[2],10),a:parseInt(c[3]||255,10)},f(c))return!0;for(;q.length;){for(h=q.pop(),i=4*(h[1]*m+h[0]);h[1]-->=0&&f(d(i));)i-=4*m;for(i+=4*m,++h[1],j=!1,k=!1;h[1]++<n-1&&f(d(i));)e(i),h[0]>0&&(f(d(i-4))?j||(q.push([h[0]-1,h[1]]),j=!0):j&&(j=!1)),h[0]<m-1&&(f(d(i+4))?k||(q.push([h[0]+1,h[1]]),k=!0):k&&(k=!1)),i+=4*m}this.putImageData(o,0,0)})}(); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/main/src/wPaint.menu.main.js b/static/js/wpaint/plugins/main/src/wPaint.menu.main.js
new file mode 100644
index 0000000..459bb73
--- /dev/null
+++ b/static/js/wpaint/plugins/main/src/wPaint.menu.main.js
@@ -0,0 +1,338 @@
+(function ($) {
+
+ // setup menu
+ $.fn.wPaint.menus.main = {
+ img: 'plugins/main/img/icons-menu-main.png',
+ items: {
+ undo: {
+ icon: 'generic',
+ title: 'Undo',
+ index: 0,
+ callback: function () { this.undo(); }
+ },
+ redo: {
+ icon: 'generic',
+ title: 'Redo',
+ index: 1,
+ callback: function () { this.redo(); }
+ },
+ clear: {
+ icon: 'generic',
+ title: 'Clear',
+ index: 2,
+ callback: function () { this.clear(); }
+ },
+ rectangle: {
+ icon: 'activate',
+ title: 'Rectangle',
+ index: 3,
+ callback: function () { this.setMode('rectangle'); }
+ },
+ ellipse: {
+ icon: 'activate',
+ title: 'Ellipse',
+ index: 4,
+ callback: function () { this.setMode('ellipse'); }
+ },
+ line: {
+ icon: 'activate',
+ title: 'Line',
+ index: 5,
+ callback: function () { this.setMode('line'); }
+ },
+ pencil: {
+ icon: 'activate',
+ title: 'Pencil',
+ index: 6,
+ callback: function () { this.setMode('pencil'); }
+ },
+ eraser: {
+ icon: 'activate',
+ title: 'Eraser',
+ index: 8,
+ callback: function () { this.setMode('eraser'); }
+ },
+ bucket: {
+ icon: 'activate',
+ title: 'Bucket',
+ index: 9,
+ callback: function () { this.setMode('bucket'); }
+ },
+ fillStyle: {
+ title: 'Fill Color',
+ icon: 'colorPicker',
+ callback: function (color) { this.setFillStyle(color); }
+ },
+ lineWidth: {
+ icon: 'select',
+ title: 'Stroke Width',
+ range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ value: 2,
+ callback: function (width) { this.setLineWidth(width); }
+ },
+ strokeStyle: {
+ icon: 'colorPicker',
+ title: 'Stroke Color',
+ callback: function (color) { this.setStrokeStyle(color); }
+ }
+ }
+ };
+
+ // extend cursors
+ $.extend($.fn.wPaint.cursors, {
+ 'default': {path: 'plugins/main/img/cursor-crosshair.png', left: 7, top: 7},
+ dropper: {path: 'plugins/main/img/cursor-dropper.png', left: 0, top: 12},
+ pencil: {path: 'plugins/main/img/cursor-pencil.png', left: 0, top: 11.99},
+ bucket: {path: 'plugins/main/img/cursor-bucket.png', left: 0, top: 10},
+ eraser1: {path: 'plugins/main/img/cursor-eraser1.png', left: 1, top: 1},
+ eraser2: {path: 'plugins/main/img/cursor-eraser2.png', left: 2, top: 2},
+ eraser3: {path: 'plugins/main/img/cursor-eraser3.png', left: 2, top: 2},
+ eraser4: {path: 'plugins/main/img/cursor-eraser4.png', left: 3, top: 3},
+ eraser5: {path: 'plugins/main/img/cursor-eraser5.png', left: 3, top: 3},
+ eraser6: {path: 'plugins/main/img/cursor-eraser6.png', left: 4, top: 4},
+ eraser7: {path: 'plugins/main/img/cursor-eraser7.png', left: 4, top: 4},
+ eraser8: {path: 'plugins/main/img/cursor-eraser8.png', left: 5, top: 5 },
+ eraser9: {path: 'plugins/main/img/cursor-eraser9.png', left: 5, top: 5},
+ eraser10: {path: 'plugins/main/img/cursor-eraser10.png', left: 6, top: 6}
+ });
+
+ // extend defaults
+ $.extend($.fn.wPaint.defaults, {
+ mode: 'pencil', // set mode
+ lineWidth: '3', // starting line width
+ fillStyle: '#FFFFFF', // starting fill style
+ strokeStyle: '#FFFF00' // start stroke style
+ });
+
+ // extend functions
+ $.fn.wPaint.extend({
+ undoCurrent: -1,
+ undoArray: [],
+ setUndoFlag: true,
+
+ generate: function () {
+ this.menus.all.main = this._createMenu('main', {
+ offsetLeft: this.options.menuOffsetLeft,
+ offsetTop: this.options.menuOffsetTop
+ });
+ },
+
+ _init: function () {
+ // must add undo on init to set the first undo as the initial drawing (bg or blank)
+ this._addUndo();
+ this.menus.all.main._setIconDisabled('clear', true);
+ },
+
+ setStrokeStyle: function (color) {
+ this.options.strokeStyle = color;
+ this.menus.all.main._setColorPickerValue('strokeStyle', color);
+ },
+
+ setLineWidth: function (width) {
+ this.options.lineWidth = width;
+ this.menus.all.main._setSelectValue('lineWidth', width);
+
+ // reset cursor here based on mode in case we need to update cursor (for instance when changing cursor for eraser sizes)
+ this.setCursor(this.options.mode);
+ },
+
+ setFillStyle: function (color) {
+ this.options.fillStyle = color;
+ this.menus.all.main._setColorPickerValue('fillStyle', color);
+ },
+
+ setCursor: function (cursor) {
+ if (cursor === 'eraser') {
+ this.setCursor('eraser' + this.options.lineWidth);
+ }
+ },
+
+ /****************************************
+ * undo / redo
+ ****************************************/
+ undo: function () {
+ if (this.undoArray[this.undoCurrent - 1]) {
+ this._setUndo(--this.undoCurrent);
+ }
+
+ this._undoToggleIcons();
+ },
+
+ redo: function () {
+ if (this.undoArray[this.undoCurrent + 1]) {
+ this._setUndo(++this.undoCurrent);
+ }
+
+ this._undoToggleIcons();
+ },
+
+ _addUndo: function () {
+
+ //if it's not at the end of the array we need to repalce the current array position
+ if (this.undoCurrent < this.undoArray.length - 1) {
+ this.undoArray[++this.undoCurrent] = this.getImage(false);
+ }
+ else { // owtherwise we push normally here
+ this.undoArray.push(this.getImage(false));
+
+ //if we're at the end of the array we need to slice off the front - in increment required
+ if (this.undoArray.length > this.undoMax) {
+ this.undoArray = this.undoArray.slice(1, this.undoArray.length);
+ }
+ //if we're NOT at the end of the array, we just increment
+ else { this.undoCurrent++; }
+ }
+
+ //for undo's then a new draw we want to remove everything afterwards - in most cases nothing will happen here
+ while (this.undoCurrent !== this.undoArray.length - 1) { this.undoArray.pop(); }
+
+ this._undoToggleIcons();
+ this.menus.all.main._setIconDisabled('clear', false);
+ },
+
+ _undoToggleIcons: function () {
+ var undoIndex = (this.undoCurrent > 0 && this.undoArray.length > 1) ? 0 : 1,
+ redoIndex = (this.undoCurrent < this.undoArray.length - 1) ? 2 : 3;
+
+ this.menus.all.main._setIconDisabled('undo', undoIndex === 1 ? true : false);
+ this.menus.all.main._setIconDisabled('redo', redoIndex === 3 ? true : false);
+ },
+
+ _setUndo: function (undoCurrent) {
+ this.setImage(this.undoArray[undoCurrent], null, null, true);
+ },
+
+ /****************************************
+ * clear
+ ****************************************/
+ clear: function () {
+
+ // only run if not disabled (make sure we only run one clear at a time)
+ if (!this.menus.all.main._isIconDisabled('clear')) {
+ this.ctx.clearRect(0, 0, this.width, this.height);
+ this._addUndo();
+ this.menus.all.main._setIconDisabled('clear', true);
+ }
+ },
+
+ /****************************************
+ * rectangle
+ ****************************************/
+ _drawRectangleDown: function (e) { this._drawShapeDown(e); },
+
+ _drawRectangleMove: function (e) {
+ this._drawShapeMove(e);
+
+ this.ctxTemp.rect(e.x, e.y, e.w, e.h);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawRectangleUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * ellipse
+ ****************************************/
+ _drawEllipseDown: function (e) { this._drawShapeDown(e); },
+
+ _drawEllipseMove: function (e) {
+ this._drawShapeMove(e);
+
+ this.ctxTemp.ellipse(e.x, e.y, e.w, e.h);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawEllipseUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * line
+ ****************************************/
+ _drawLineDown: function (e) { this._drawShapeDown(e); },
+
+ _drawLineMove: function (e) {
+ this._drawShapeMove(e, 1);
+
+ var xo = this.canvasTempLeftOriginal;
+ var yo = this.canvasTempTopOriginal;
+
+ if (e.pageX < xo) { e.x = e.x + e.w; e.w = e.w * - 1; }
+ if (e.pageY < yo) { e.y = e.y + e.h; e.h = e.h * - 1; }
+
+ this.ctxTemp.lineJoin = 'round';
+ this.ctxTemp.beginPath();
+ this.ctxTemp.moveTo(e.x, e.y);
+ this.ctxTemp.lineTo(e.x + e.w, e.y + e.h);
+ this.ctxTemp.closePath();
+ this.ctxTemp.stroke();
+ },
+
+ _drawLineUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * pencil
+ ****************************************/
+ _drawPencilDown: function (e) {
+ this.ctx.lineJoin = 'round';
+ this.ctx.lineCap = 'round';
+ this.ctx.strokeStyle = this.options.strokeStyle;
+ this.ctx.fillStyle = this.options.strokeStyle;
+ this.ctx.lineWidth = this.options.lineWidth;
+
+ //draw single dot in case of a click without a move
+ this.ctx.beginPath();
+ this.ctx.arc(e.pageX, e.pageY, this.options.lineWidth / 2, 0, Math.PI * 2, true);
+ this.ctx.closePath();
+ this.ctx.fill();
+
+ //start the path for a drag
+ this.ctx.beginPath();
+ this.ctx.moveTo(e.pageX, e.pageY);
+ },
+
+ _drawPencilMove: function (e) {
+ this.ctx.lineTo(e.pageX, e.pageY);
+ this.ctx.stroke();
+ },
+
+ _drawPencilUp: function () {
+ this.ctx.closePath();
+ this._addUndo();
+ },
+
+ /****************************************
+ * eraser
+ ****************************************/
+ _drawEraserDown: function (e) {
+ this.ctx.save();
+ this.ctx.globalCompositeOperation = 'destination-out';
+ this._drawPencilDown(e);
+ },
+
+ _drawEraserMove: function (e) {
+ this._drawPencilMove(e);
+ },
+
+ _drawEraserUp: function (e) {
+ this._drawPencilUp(e);
+ this.ctx.restore();
+ },
+
+ /****************************************
+ * bucket
+ ****************************************/
+ _drawBucketDown: function (e) {
+ this.ctx.fillArea(e.pageX, e.pageY, this.options.fillStyle);
+ this._addUndo();
+ }
+ });
+})(jQuery);
diff --git a/static/js/wpaint/plugins/main/wPaint.menu.main.min.js b/static/js/wpaint/plugins/main/wPaint.menu.main.min.js
new file mode 100644
index 0000000..0a950c5
--- /dev/null
+++ b/static/js/wpaint/plugins/main/wPaint.menu.main.min.js
@@ -0,0 +1 @@
+/*! wPaint - v2.5.0 - 2014-03-01 */!function(a){a.fn.wPaint.menus.main={img:"plugins/main/img/icons-menu-main.png",items:{undo:{icon:"generic",title:"Undo",index:0,callback:function(){this.undo()}},redo:{icon:"generic",title:"Redo",index:1,callback:function(){this.redo()}},clear:{icon:"generic",title:"Clear",index:2,callback:function(){this.clear()}},rectangle:{icon:"activate",title:"Rectangle",index:3,callback:function(){this.setMode("rectangle")}},ellipse:{icon:"activate",title:"Ellipse",index:4,callback:function(){this.setMode("ellipse")}},line:{icon:"activate",title:"Line",index:5,callback:function(){this.setMode("line")}},pencil:{icon:"activate",title:"Pencil",index:6,callback:function(){this.setMode("pencil")}},eraser:{icon:"activate",title:"Eraser",index:8,callback:function(){this.setMode("eraser")}},bucket:{icon:"activate",title:"Bucket",index:9,callback:function(){this.setMode("bucket")}},fillStyle:{title:"Fill Color",icon:"colorPicker",callback:function(a){this.setFillStyle(a)}},lineWidth:{icon:"select",title:"Stroke Width",range:[1,2,3,4,5,6,7,8,9,10],value:2,callback:function(a){this.setLineWidth(a)}},strokeStyle:{icon:"colorPicker",title:"Stroke Color",callback:function(a){this.setStrokeStyle(a)}}}},a.extend(a.fn.wPaint.cursors,{"default":{path:"plugins/main/img/cursor-crosshair.png",left:7,top:7},dropper:{path:"plugins/main/img/cursor-dropper.png",left:0,top:12},pencil:{path:"plugins/main/img/cursor-pencil.png",left:0,top:11.99},bucket:{path:"plugins/main/img/cursor-bucket.png",left:0,top:10},eraser1:{path:"plugins/main/img/cursor-eraser1.png",left:1,top:1},eraser2:{path:"plugins/main/img/cursor-eraser2.png",left:2,top:2},eraser3:{path:"plugins/main/img/cursor-eraser3.png",left:2,top:2},eraser4:{path:"plugins/main/img/cursor-eraser4.png",left:3,top:3},eraser5:{path:"plugins/main/img/cursor-eraser5.png",left:3,top:3},eraser6:{path:"plugins/main/img/cursor-eraser6.png",left:4,top:4},eraser7:{path:"plugins/main/img/cursor-eraser7.png",left:4,top:4},eraser8:{path:"plugins/main/img/cursor-eraser8.png",left:5,top:5},eraser9:{path:"plugins/main/img/cursor-eraser9.png",left:5,top:5},eraser10:{path:"plugins/main/img/cursor-eraser10.png",left:6,top:6}}),a.extend(a.fn.wPaint.defaults,{mode:"pencil",lineWidth:"3",fillStyle:"#FFFFFF",strokeStyle:"#FFFF00"}),a.fn.wPaint.extend({undoCurrent:-1,undoArray:[],setUndoFlag:!0,generate:function(){this.menus.all.main=this._createMenu("main",{offsetLeft:this.options.menuOffsetLeft,offsetTop:this.options.menuOffsetTop})},_init:function(){this._addUndo(),this.menus.all.main._setIconDisabled("clear",!0)},setStrokeStyle:function(a){this.options.strokeStyle=a,this.menus.all.main._setColorPickerValue("strokeStyle",a)},setLineWidth:function(a){this.options.lineWidth=a,this.menus.all.main._setSelectValue("lineWidth",a),this.setCursor(this.options.mode)},setFillStyle:function(a){this.options.fillStyle=a,this.menus.all.main._setColorPickerValue("fillStyle",a)},setCursor:function(a){"eraser"===a&&this.setCursor("eraser"+this.options.lineWidth)},undo:function(){this.undoArray[this.undoCurrent-1]&&this._setUndo(--this.undoCurrent),this._undoToggleIcons()},redo:function(){this.undoArray[this.undoCurrent+1]&&this._setUndo(++this.undoCurrent),this._undoToggleIcons()},_addUndo:function(){for(this.undoCurrent<this.undoArray.length-1?this.undoArray[++this.undoCurrent]=this.getImage(!1):(this.undoArray.push(this.getImage(!1)),this.undoArray.length>this.undoMax?this.undoArray=this.undoArray.slice(1,this.undoArray.length):this.undoCurrent++);this.undoCurrent!==this.undoArray.length-1;)this.undoArray.pop();this._undoToggleIcons(),this.menus.all.main._setIconDisabled("clear",!1)},_undoToggleIcons:function(){var a=this.undoCurrent>0&&this.undoArray.length>1?0:1,b=this.undoCurrent<this.undoArray.length-1?2:3;this.menus.all.main._setIconDisabled("undo",1===a?!0:!1),this.menus.all.main._setIconDisabled("redo",3===b?!0:!1)},_setUndo:function(a){this.setImage(this.undoArray[a],null,null,!0)},clear:function(){this.menus.all.main._isIconDisabled("clear")||(this.ctx.clearRect(0,0,this.width,this.height),this._addUndo(),this.menus.all.main._setIconDisabled("clear",!0))},_drawRectangleDown:function(a){this._drawShapeDown(a)},_drawRectangleMove:function(a){this._drawShapeMove(a),this.ctxTemp.rect(a.x,a.y,a.w,a.h),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawRectangleUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawEllipseDown:function(a){this._drawShapeDown(a)},_drawEllipseMove:function(a){this._drawShapeMove(a),this.ctxTemp.ellipse(a.x,a.y,a.w,a.h),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawEllipseUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawLineDown:function(a){this._drawShapeDown(a)},_drawLineMove:function(a){this._drawShapeMove(a,1);var b=this.canvasTempLeftOriginal,c=this.canvasTempTopOriginal;a.pageX<b&&(a.x=a.x+a.w,a.w=-1*a.w),a.pageY<c&&(a.y=a.y+a.h,a.h=-1*a.h),this.ctxTemp.lineJoin="round",this.ctxTemp.beginPath(),this.ctxTemp.moveTo(a.x,a.y),this.ctxTemp.lineTo(a.x+a.w,a.y+a.h),this.ctxTemp.closePath(),this.ctxTemp.stroke()},_drawLineUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawPencilDown:function(a){this.ctx.lineJoin="round",this.ctx.lineCap="round",this.ctx.strokeStyle=this.options.strokeStyle,this.ctx.fillStyle=this.options.strokeStyle,this.ctx.lineWidth=this.options.lineWidth,this.ctx.beginPath(),this.ctx.arc(a.pageX,a.pageY,this.options.lineWidth/2,0,2*Math.PI,!0),this.ctx.closePath(),this.ctx.fill(),this.ctx.beginPath(),this.ctx.moveTo(a.pageX,a.pageY)},_drawPencilMove:function(a){this.ctx.lineTo(a.pageX,a.pageY),this.ctx.stroke()},_drawPencilUp:function(){this.ctx.closePath(),this._addUndo()},_drawEraserDown:function(a){this.ctx.save(),this.ctx.globalCompositeOperation="destination-out",this._drawPencilDown(a)},_drawEraserMove:function(a){this._drawPencilMove(a)},_drawEraserUp:function(a){this._drawPencilUp(a),this.ctx.restore()},_drawBucketDown:function(a){this.ctx.fillArea(a.pageX,a.pageY,this.options.fillStyle),this._addUndo()}})}(jQuery),!function(){window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.fillArea=function(a,b,c){function d(a){return{r:p[a],g:p[a+1],b:p[a+2],a:p[a+3]}}function e(a){p[a]=c.r,p[a+1]=c.g,p[a+2]=c.b,p[a+3]=c.a}function f(a){return g.r===a.r&&g.g===a.g&&g.b===a.b&&g.a===a.a}if(!a||!b||!c)return!0;var g,h,i,j,k,l,m=this.canvas.width,n=this.canvas.height,o=this.getImageData(0,0,m,n),p=o.data,q=[[a,b]];if(g=d(4*(b*m+a)),l=this.canvas.style.color,this.canvas.style.color=c,c=this.canvas.style.color.match(/^rgba?\((.*)\);?$/)[1].split(","),this.canvas.style.color=l,c={r:parseInt(c[0],10),g:parseInt(c[1],10),b:parseInt(c[2],10),a:parseInt(c[3]||255,10)},f(c))return!0;for(;q.length;){for(h=q.pop(),i=4*(h[1]*m+h[0]);h[1]-->=0&&f(d(i));)i-=4*m;for(i+=4*m,++h[1],j=!1,k=!1;h[1]++<n-1&&f(d(i));)e(i),h[0]>0&&(f(d(i-4))?j||(q.push([h[0]-1,h[1]]),j=!0):j&&(j=!1)),h[0]<m-1&&(f(d(i+4))?k||(q.push([h[0]+1,h[1]]),k=!0):k&&(k=!1)),i+=4*m}this.putImageData(o,0,0)})}(); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/shapes/img/icons-menu-main-shapes.png b/static/js/wpaint/plugins/shapes/img/icons-menu-main-shapes.png
new file mode 100644
index 0000000..b2cb874
--- /dev/null
+++ b/static/js/wpaint/plugins/shapes/img/icons-menu-main-shapes.png
Binary files differ
diff --git a/static/js/wpaint/plugins/shapes/src/shapes.min.js b/static/js/wpaint/plugins/shapes/src/shapes.min.js
new file mode 100644
index 0000000..585aa85
--- /dev/null
+++ b/static/js/wpaint/plugins/shapes/src/shapes.min.js
@@ -0,0 +1 @@
+!function(){window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.diamond=function(a,b,c,d){return a&&b&&c&&d?(this.beginPath(),this.moveTo(a+.5*c,b),this.lineTo(a,b+.5*d),this.lineTo(a+.5*c,b+d),this.lineTo(a+c,b+.5*d),this.lineTo(a+.5*c,b),this.closePath(),void 0):!0}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.ellipse=function(a,b,c,d){if(!(a&&b&&c&&d))return!0;var e=.5522848,f=c/2*e,g=d/2*e,h=a+c,i=b+d,j=a+c/2,k=b+d/2;this.beginPath(),this.moveTo(a,k),this.bezierCurveTo(a,k-g,j-f,b,j,b),this.bezierCurveTo(j+f,b,h,k-g,h,k),this.bezierCurveTo(h,k+g,j+f,i,j,i),this.bezierCurveTo(j-f,i,a,k+g,a,k),this.closePath()}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.hexagon=function(a,b,c,d){if(!(a&&b&&c&&d))return!0;var e=.225,f=1-e;this.beginPath(),this.moveTo(a+.5*c,b),this.lineTo(a,b+d*e),this.lineTo(a,b+d*f),this.lineTo(a+.5*c,b+d),this.lineTo(a+c,b+d*f),this.lineTo(a+c,b+d*e),this.lineTo(a+.5*c,b),this.closePath()}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.pentagon=function(a,b,c,d){return a&&b&&c&&d?(this.beginPath(),this.moveTo(a+c/2,b),this.lineTo(a,b+.4*d),this.lineTo(a+.2*c,b+d),this.lineTo(a+.8*c,b+d),this.lineTo(a+c,b+.4*d),this.lineTo(a+c/2,b),this.closePath(),void 0):!0}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.roundedRect=function(a,b,c,d,e){return a&&b&&c&&d?(e||(e=5),this.beginPath(),this.moveTo(a+e,b),this.lineTo(a+c-e,b),this.quadraticCurveTo(a+c,b,a+c,b+e),this.lineTo(a+c,b+d-e),this.quadraticCurveTo(a+c,b+d,a+c-e,b+d),this.lineTo(a+e,b+d),this.quadraticCurveTo(a,b+d,a,b+d-e),this.lineTo(a,b+e),this.quadraticCurveTo(a,b,a+e,b),this.closePath(),void 0):!0})}(); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/shapes/src/wPaint.menu.main.shapes.js b/static/js/wpaint/plugins/shapes/src/wPaint.menu.main.shapes.js
new file mode 100644
index 0000000..1d4daeb
--- /dev/null
+++ b/static/js/wpaint/plugins/shapes/src/wPaint.menu.main.shapes.js
@@ -0,0 +1,207 @@
+(function ($) {
+ var img = 'plugins/shapes/img/icons-menu-main-shapes.png';
+
+ // extend menu
+ $.extend(true, $.fn.wPaint.menus.main.items, {
+ rectangle: {
+ group: 'shapes'
+ },
+ roundedRect: {
+ icon: 'activate',
+ group: 'shapes',
+ title: 'Rounded Rectangle',
+ img: img,
+ index: 0,
+ callback: function () { this.setMode('roundedRect'); }
+ },
+ square: {
+ icon: 'activate',
+ group: 'shapes',
+ title: 'Square',
+ img: img,
+ index: 1,
+ callback: function () { this.setMode('square'); }
+ },
+ roundedSquare: {
+ icon: 'activate',
+ group: 'shapes',
+ title: 'Rounded Square',
+ img: img,
+ index: 2,
+ callback: function () { this.setMode('roundedSquare'); }
+ },
+ diamond: {
+ icon: 'activate',
+ group: 'shapes',
+ title: 'Diamond',
+ img: img,
+ index: 4,
+ callback: function () { this.setMode('diamond'); }
+ },
+
+ ellipse: {
+ group: 'shapes2'
+ },
+ circle: {
+ icon: 'activate',
+ group: 'shapes2',
+ title: 'Circle',
+ img: img,
+ index: 3,
+ callback: function () { this.setMode('circle'); }
+ },
+ pentagon: {
+ icon: 'activate',
+ group: 'shapes2',
+ title: 'Pentagon',
+ img: img,
+ index: 5,
+ callback: function () { this.setMode('pentagon'); }
+ },
+ hexagon: {
+ icon: 'activate',
+ group: 'shapes2',
+ title: 'Hexagon',
+ img: img,
+ index: 6,
+ callback: function () { this.setMode('hexagon'); }
+ }
+ });
+
+ // extend functions
+ $.fn.wPaint.extend({
+ /****************************************
+ * roundedRect
+ ****************************************/
+ _drawRoundedRectDown: function (e) { this._drawShapeDown(e); },
+
+ _drawRoundedRectMove: function (e) {
+ this._drawShapeMove(e);
+
+ var radius = e.w > e.h ? e.h / e.w : e.w / e.h;
+
+ this.ctxTemp.roundedRect(e.x, e.y, e.w, e.h, Math.ceil(radius * (e.w * e.h * 0.001)));
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawRoundedRectUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * square
+ ****************************************/
+ _drawSquareDown: function (e) { this._drawShapeDown(e); },
+
+ _drawSquareMove: function (e) {
+ this._drawShapeMove(e);
+
+ var l = e.w > e.h ? e.h : e.w;
+
+ this.ctxTemp.rect(e.x, e.y, l, l);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawSquareUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * roundedSquare
+ ****************************************/
+ _drawRoundedSquareDown: function (e) { this._drawShapeDown(e); },
+
+ _drawRoundedSquareMove: function (e) {
+ this._drawShapeMove(e);
+
+ var l = e.w > e.h ? e.h : e.w;
+
+ this.ctxTemp.roundedRect(e.x, e.y, l, l, Math.ceil(l * l * 0.001));
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawRoundedSquareUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * diamond
+ ****************************************/
+ _drawDiamondDown: function (e) { this._drawShapeDown(e); },
+
+ _drawDiamondMove: function (e) {
+ this._drawShapeMove(e);
+
+ this.ctxTemp.diamond(e.x, e.y, e.w, e.h);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawDiamondUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * circle
+ ****************************************/
+ _drawCircleDown: function (e) { this._drawShapeDown(e); },
+
+ _drawCircleMove: function (e) {
+ this._drawShapeMove(e);
+
+ var l = e.w > e.h ? e.h : e.w;
+
+ this.ctxTemp.ellipse(e.x, e.y, l, l);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawCircleUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * pentagon
+ ****************************************/
+ _drawPentagonDown: function (e) { this._drawShapeDown(e); },
+
+ _drawPentagonMove: function (e) {
+ this._drawShapeMove(e);
+
+ this.ctxTemp.pentagon(e.x, e.y, e.w, e.h);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawPentagonUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ },
+
+ /****************************************
+ * hexagon
+ ****************************************/
+ _drawHexagonDown: function (e) { this._drawShapeDown(e); },
+
+ _drawHexagonMove: function (e) {
+ this._drawShapeMove(e);
+
+ this.ctxTemp.hexagon(e.x, e.y, e.w, e.h);
+ this.ctxTemp.stroke();
+ this.ctxTemp.fill();
+ },
+
+ _drawHexagonUp: function (e) {
+ this._drawShapeUp(e);
+ this._addUndo();
+ }
+ });
+})(jQuery);
diff --git a/static/js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js b/static/js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js
new file mode 100644
index 0000000..ac765bd
--- /dev/null
+++ b/static/js/wpaint/plugins/shapes/wPaint.menu.main.shapes.min.js
@@ -0,0 +1 @@
+/*! wPaint - v2.5.0 - 2014-03-01 */!function(a){var b="plugins/shapes/img/icons-menu-main-shapes.png";a.extend(!0,a.fn.wPaint.menus.main.items,{rectangle:{group:"shapes"},roundedRect:{icon:"activate",group:"shapes",title:"Rounded Rectangle",img:b,index:0,callback:function(){this.setMode("roundedRect")}},square:{icon:"activate",group:"shapes",title:"Square",img:b,index:1,callback:function(){this.setMode("square")}},roundedSquare:{icon:"activate",group:"shapes",title:"Rounded Square",img:b,index:2,callback:function(){this.setMode("roundedSquare")}},diamond:{icon:"activate",group:"shapes",title:"Diamond",img:b,index:4,callback:function(){this.setMode("diamond")}},ellipse:{group:"shapes2"},circle:{icon:"activate",group:"shapes2",title:"Circle",img:b,index:3,callback:function(){this.setMode("circle")}},pentagon:{icon:"activate",group:"shapes2",title:"Pentagon",img:b,index:5,callback:function(){this.setMode("pentagon")}},hexagon:{icon:"activate",group:"shapes2",title:"Hexagon",img:b,index:6,callback:function(){this.setMode("hexagon")}}}),a.fn.wPaint.extend({_drawRoundedRectDown:function(a){this._drawShapeDown(a)},_drawRoundedRectMove:function(a){this._drawShapeMove(a);var b=a.w>a.h?a.h/a.w:a.w/a.h;this.ctxTemp.roundedRect(a.x,a.y,a.w,a.h,Math.ceil(b*a.w*a.h*.001)),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawRoundedRectUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawSquareDown:function(a){this._drawShapeDown(a)},_drawSquareMove:function(a){this._drawShapeMove(a);var b=a.w>a.h?a.h:a.w;this.ctxTemp.rect(a.x,a.y,b,b),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawSquareUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawRoundedSquareDown:function(a){this._drawShapeDown(a)},_drawRoundedSquareMove:function(a){this._drawShapeMove(a);var b=a.w>a.h?a.h:a.w;this.ctxTemp.roundedRect(a.x,a.y,b,b,Math.ceil(b*b*.001)),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawRoundedSquareUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawDiamondDown:function(a){this._drawShapeDown(a)},_drawDiamondMove:function(a){this._drawShapeMove(a),this.ctxTemp.diamond(a.x,a.y,a.w,a.h),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawDiamondUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawCircleDown:function(a){this._drawShapeDown(a)},_drawCircleMove:function(a){this._drawShapeMove(a);var b=a.w>a.h?a.h:a.w;this.ctxTemp.ellipse(a.x,a.y,b,b),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawCircleUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawPentagonDown:function(a){this._drawShapeDown(a)},_drawPentagonMove:function(a){this._drawShapeMove(a),this.ctxTemp.pentagon(a.x,a.y,a.w,a.h),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawPentagonUp:function(a){this._drawShapeUp(a),this._addUndo()},_drawHexagonDown:function(a){this._drawShapeDown(a)},_drawHexagonMove:function(a){this._drawShapeMove(a),this.ctxTemp.hexagon(a.x,a.y,a.w,a.h),this.ctxTemp.stroke(),this.ctxTemp.fill()},_drawHexagonUp:function(a){this._drawShapeUp(a),this._addUndo()}})}(jQuery),!function(){window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.diamond=function(a,b,c,d){return a&&b&&c&&d?(this.beginPath(),this.moveTo(a+.5*c,b),this.lineTo(a,b+.5*d),this.lineTo(a+.5*c,b+d),this.lineTo(a+c,b+.5*d),this.lineTo(a+.5*c,b),void this.closePath()):!0}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.ellipse=function(a,b,c,d){if(!(a&&b&&c&&d))return!0;var e=.5522848,f=c/2*e,g=d/2*e,h=a+c,i=b+d,j=a+c/2,k=b+d/2;this.beginPath(),this.moveTo(a,k),this.bezierCurveTo(a,k-g,j-f,b,j,b),this.bezierCurveTo(j+f,b,h,k-g,h,k),this.bezierCurveTo(h,k+g,j+f,i,j,i),this.bezierCurveTo(j-f,i,a,k+g,a,k),this.closePath()}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.hexagon=function(a,b,c,d){if(!(a&&b&&c&&d))return!0;var e=.225,f=1-e;this.beginPath(),this.moveTo(a+.5*c,b),this.lineTo(a,b+d*e),this.lineTo(a,b+d*f),this.lineTo(a+.5*c,b+d),this.lineTo(a+c,b+d*f),this.lineTo(a+c,b+d*e),this.lineTo(a+.5*c,b),this.closePath()}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.pentagon=function(a,b,c,d){return a&&b&&c&&d?(this.beginPath(),this.moveTo(a+c/2,b),this.lineTo(a,b+.4*d),this.lineTo(a+.2*c,b+d),this.lineTo(a+.8*c,b+d),this.lineTo(a+c,b+.4*d),this.lineTo(a+c/2,b),void this.closePath()):!0}),window.CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.roundedRect=function(a,b,c,d,e){return a&&b&&c&&d?(e||(e=5),this.beginPath(),this.moveTo(a+e,b),this.lineTo(a+c-e,b),this.quadraticCurveTo(a+c,b,a+c,b+e),this.lineTo(a+c,b+d-e),this.quadraticCurveTo(a+c,b+d,a+c-e,b+d),this.lineTo(a+e,b+d),this.quadraticCurveTo(a,b+d,a,b+d-e),this.lineTo(a,b+e),this.quadraticCurveTo(a,b,a+e,b),void this.closePath()):!0})}(); \ No newline at end of file
diff --git a/static/js/wpaint/plugins/text/img/icons-menu-text.png b/static/js/wpaint/plugins/text/img/icons-menu-text.png
new file mode 100644
index 0000000..d7bed8c
--- /dev/null
+++ b/static/js/wpaint/plugins/text/img/icons-menu-text.png
Binary files differ
diff --git a/static/js/wpaint/plugins/text/src/wPaint.menu.text.js b/static/js/wpaint/plugins/text/src/wPaint.menu.text.js
new file mode 100644
index 0000000..5a15ac0
--- /dev/null
+++ b/static/js/wpaint/plugins/text/src/wPaint.menu.text.js
@@ -0,0 +1,227 @@
+(function ($) {
+
+ // setup menu
+ $.fn.wPaint.menus.text = {
+ img: 'plugins/text/img/icons-menu-text.png',
+ items: {
+ bold: {
+ icon: 'toggle',
+ title: 'Bold',
+ index: 0,
+ callback: function (toggle) { this.setFontBold(toggle); }
+ },
+ italic: {
+ icon: 'toggle',
+ title: 'Italic',
+ index: 1,
+ callback: function (toggle) { this.setFontItalic(toggle); }
+ },
+ /*underline: {
+ icon: 'toggle',
+ title: 'Undelrine',
+ index: 2,
+ callback: function (toggle) { this.setFontUnderline(toggle); }
+ },*/
+ fontSize: {
+ title: 'Font Size',
+ icon: 'select',
+ range: [8, 9, 10, 12, 14, 16, 20, 24, 30],
+ value: 12,
+ callback: function (size) { this.setFontSize(size); }
+ },
+ fontFamily: {
+ icon: 'select',
+ title: 'Font Family',
+ range: ['Arial', 'Courier', 'Times', 'Verdana'],
+ useRange: true,
+ value: 'Arial',
+ callback: function (family) { this.setFontFamily(family); }
+ }
+ }
+ };
+
+ // add icon to main menu
+ $.fn.wPaint.menus.main.items.text = {
+ icon: 'menu',
+ after: 'pencil',
+ title: 'Text',
+ index: 7,
+ callback: function () { this.setMode('text'); }
+ };
+
+ // extend defaults
+ $.extend($.fn.wPaint.defaults, {
+ fontSize : '12', // current font size for text input
+ fontFamily : 'Arial', // active font family for text input
+ fontBold : false, // text input bold enable/disable
+ fontItalic : false, // text input italic enable/disable
+ fontUnderline : false // text input italic enable/disable
+ });
+
+ // extend functions
+ $.fn.wPaint.extend({
+ generate: function () {
+ this.$textCalc = $('<div></div>').hide();
+
+ // make sure clicking on the text-tnput doesn't trigger another textInput
+ this.$textInput = $('<textarea class="wPaint-text-input" spellcheck="false"></textarea>')
+ .on('mousedown', this._stopPropagation)
+ .css({position: 'absolute'})
+ .hide();
+
+ $('body').append(this.$textCalc);
+ this.$el.append(this.$textInput);
+
+ this.menus.all.text = this._createMenu('text');
+ },
+
+ _init: function () {
+ var _this = this;
+
+ function inputClick() {
+ _this._drawTextIfNotEmpty();
+ _this.$textInput.hide();
+ _this.$canvasTemp.hide();
+ }
+
+ // in case we click on another element while typing - just auto set the text
+ for (var i in this.menus.all) {
+ this.menus.all[i].$menu
+ .on('click', inputClick)
+ .on('mousedown', this._stopPropagation);
+ }
+
+ // same idea here for clicking outside of the canvas area
+ $(document).on('mousedown', inputClick);
+ },
+
+ /****************************************
+ * setters
+ ****************************************/
+ setFillStyle: function (fillStyle) {
+ this.$textInput.css('color', fillStyle);
+ },
+
+ setFontSize: function (size) {
+ this.options.fontSize = parseInt(size, 10);
+ this._setFont({fontSize: size + 'px', lineHeight: size + 'px'});
+ this.menus.all.text._setSelectValue('fontSize', size);
+ },
+
+ setFontFamily: function (family) {
+ this.options.fontFamily = family;
+ this._setFont({fontFamily: family});
+ this.menus.all.text._setSelectValue('fontFamily', family);
+ },
+
+ setFontBold: function (bold) {
+ this.options.fontBold = bold;
+ this._setFont({fontWeight: (bold ? 'bold' : '')});
+ },
+
+ setFontItalic: function (italic) {
+ this.options.fontItalic = italic;
+ this._setFont({fontStyle: (italic ? 'italic' : '')});
+ },
+
+ setFontUnderline: function (underline) {
+ this.options.fontUnderline = underline;
+ this._setFont({fontWeight: (underline ? 'underline' : '')});
+ },
+
+ _setFont: function (css) {
+ this.$textInput.css(css);
+ this.$textCalc.css(css);
+ },
+
+ /****************************************
+ * Text
+ ****************************************/
+ _drawTextDown: function (e) {
+ this._drawTextIfNotEmpty();
+ this._drawShapeDown(e, 1);
+
+ this.$textInput
+ .css({left: e.pageX - 1, top: e.pageY - 1, width: 0, height: 0})
+ .show().focus();
+ },
+
+ _drawTextMove: function (e) {
+ this._drawShapeMove(e, 1);
+
+ this.$textInput.css({left: e.left - 1, top: e.top - 1, width: e.width, height: e.height});
+ },
+
+ _drawTextIfNotEmpty: function () {
+ if (this.$textInput.val() !== '') { this._drawText(); }
+ },
+
+ // just draw text - don't want to trigger up here since we are just copying text from input box here
+ _drawText: function () {
+ var fontString = '',
+ lines = this.$textInput.val().split('\n'),
+ linesNew = [],
+ textInputWidth = this.$textInput.width() - 2,
+ width = 0,
+ lastj = 0,
+ offset = this.$textInput.position(),
+ left = offset.left + 1,
+ top = offset.top + 1,
+ //underlineOffset = 0,
+ i, ii, j, jj;
+
+ if (this.options.fontItalic) { fontString += 'italic '; }
+ //if(this.settings.fontUnderline) { fontString += 'underline '; }
+ if (this.options.fontBold) { fontString += 'bold '; }
+
+ fontString += this.options.fontSize + 'px ' + this.options.fontFamily;
+
+ for (i = 0, ii = lines.length; i < ii; i++) {
+ this.$textCalc.html('');
+ lastj = 0;
+
+ for (j = 0, jj = lines[0].length; j < jj; j++) {
+ width = this.$textCalc.append(lines[i][j]).width();
+
+ if (width > textInputWidth) {
+ linesNew.push(lines[i].substring(lastj, j));
+ lastj = j;
+ this.$textCalc.html(lines[i][j]);
+ }
+ }
+
+ if (lastj !== j) { linesNew.push(lines[i].substring(lastj, j)); }
+ }
+
+ lines = this.$textInput.val(linesNew.join('\n')).val().split('\n');
+
+ for (i = 0, ii = lines.length; i < ii; i++) {
+ this.ctx.fillStyle = this.options.fillStyle;
+ this.ctx.textBaseline = 'top';
+ this.ctx.font = fontString;
+ this.ctx.fillText(lines[i], left, top);
+
+ top += this.options.fontSize;
+
+ /*if(lines[i] !== '' && this.options.fontTypeUnderline) {
+ width = this.$textCalc.html(lines[i]).width();
+
+ //manually set pixels for underline since to avoid antialiasing 1px issue, and lack of support for underline in canvas
+ var imgData = this.ctx.getImageData(0, top+underlineOffset, width, 1);
+
+ for (j=0; j<imgData.width*imgData.height*4; j+=4) {
+ imgData.data[j] = parseInt(this.options.fillStyle.substring(1,3), 16);
+ imgData.data[j+1] = parseInt(this.options.fillStyle.substring(3,5), 16);
+ imgData.data[j+2] = parseInt(this.options.fillStyle.substring(5,7), 16);
+ imgData.data[j+3] = 255;
+ }
+
+ this.ctx.putImageData(imgData, left, top+underlineOffset);
+ }*/
+ }
+
+ this.$textInput.val('');
+ this._addUndo();
+ }
+ });
+})(jQuery);
diff --git a/static/js/wpaint/plugins/text/wPaint.menu.text.min.js b/static/js/wpaint/plugins/text/wPaint.menu.text.min.js
new file mode 100644
index 0000000..6cd1f3d
--- /dev/null
+++ b/static/js/wpaint/plugins/text/wPaint.menu.text.min.js
@@ -0,0 +1 @@
+/*! wPaint - v2.5.0 - 2014-03-01 */!function(a){a.fn.wPaint.menus.text={img:"plugins/text/img/icons-menu-text.png",items:{bold:{icon:"toggle",title:"Bold",index:0,callback:function(a){this.setFontBold(a)}},italic:{icon:"toggle",title:"Italic",index:1,callback:function(a){this.setFontItalic(a)}},fontSize:{title:"Font Size",icon:"select",range:[8,9,10,12,14,16,20,24,30],value:12,callback:function(a){this.setFontSize(a)}},fontFamily:{icon:"select",title:"Font Family",range:["Arial","Courier","Times","Verdana"],useRange:!0,value:"Arial",callback:function(a){this.setFontFamily(a)}}}},a.fn.wPaint.menus.main.items.text={icon:"menu",after:"pencil",title:"Text",index:7,callback:function(){this.setMode("text")}},a.extend(a.fn.wPaint.defaults,{fontSize:"12",fontFamily:"Arial",fontBold:!1,fontItalic:!1,fontUnderline:!1}),a.fn.wPaint.extend({generate:function(){this.$textCalc=a("<div></div>").hide(),this.$textInput=a('<textarea class="wPaint-text-input" spellcheck="false"></textarea>').on("mousedown",this._stopPropagation).css({position:"absolute"}).hide(),a("body").append(this.$textCalc),this.$el.append(this.$textInput),this.menus.all.text=this._createMenu("text")},_init:function(){function b(){c._drawTextIfNotEmpty(),c.$textInput.hide(),c.$canvasTemp.hide()}var c=this;for(var d in this.menus.all)this.menus.all[d].$menu.on("click",b).on("mousedown",this._stopPropagation);a(document).on("mousedown",b)},setFillStyle:function(a){this.$textInput.css("color",a)},setFontSize:function(a){this.options.fontSize=parseInt(a,10),this._setFont({fontSize:a+"px",lineHeight:a+"px"}),this.menus.all.text._setSelectValue("fontSize",a)},setFontFamily:function(a){this.options.fontFamily=a,this._setFont({fontFamily:a}),this.menus.all.text._setSelectValue("fontFamily",a)},setFontBold:function(a){this.options.fontBold=a,this._setFont({fontWeight:a?"bold":""})},setFontItalic:function(a){this.options.fontItalic=a,this._setFont({fontStyle:a?"italic":""})},setFontUnderline:function(a){this.options.fontUnderline=a,this._setFont({fontWeight:a?"underline":""})},_setFont:function(a){this.$textInput.css(a),this.$textCalc.css(a)},_drawTextDown:function(a){this._drawTextIfNotEmpty(),this._drawShapeDown(a,1),this.$textInput.css({left:a.pageX-1,top:a.pageY-1,width:0,height:0}).show().focus()},_drawTextMove:function(a){this._drawShapeMove(a,1),this.$textInput.css({left:a.left-1,top:a.top-1,width:a.width,height:a.height})},_drawTextIfNotEmpty:function(){""!==this.$textInput.val()&&this._drawText()},_drawText:function(){var a,b,c,d,e="",f=this.$textInput.val().split("\n"),g=[],h=this.$textInput.width()-2,i=0,j=0,k=this.$textInput.position(),l=k.left+1,m=k.top+1;for(this.options.fontItalic&&(e+="italic "),this.options.fontBold&&(e+="bold "),e+=this.options.fontSize+"px "+this.options.fontFamily,a=0,b=f.length;b>a;a++){for(this.$textCalc.html(""),j=0,c=0,d=f[0].length;d>c;c++)i=this.$textCalc.append(f[a][c]).width(),i>h&&(g.push(f[a].substring(j,c)),j=c,this.$textCalc.html(f[a][c]));j!==c&&g.push(f[a].substring(j,c))}for(f=this.$textInput.val(g.join("\n")).val().split("\n"),a=0,b=f.length;b>a;a++)this.ctx.fillStyle=this.options.fillStyle,this.ctx.textBaseline="top",this.ctx.font=e,this.ctx.fillText(f[a],l,m),m+=this.options.fontSize;this.$textInput.val(""),this._addUndo()}})}(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/src/wPaint.css b/static/js/wpaint/src/wPaint.css
new file mode 100644
index 0000000..7bc0a70
--- /dev/null
+++ b/static/js/wpaint/src/wPaint.css
@@ -0,0 +1,348 @@
+/**********************************************************************
+ * Layout
+ **********************************************************************/
+
+/*** menu ***/
+.wPaint-menu {
+ position: absolute !important;
+ display: inline-block;
+ line-height: 0px;
+ z-index: 99;
+}
+.wPaint-menu-behind {
+ z-index: 98;
+}
+.wPaint-menu-holder {
+ position: relative;
+ margin: 0 1px 1px 0;
+}
+.wPaint-menu-handle {
+ display: inline-block;
+}
+.wPaint-menu-icon {
+ position: relative;
+ vertical-align: top;
+}
+.wPaint-menu-icon-img {
+ position: relative;
+ display: inline-block;
+ background-repeat: no-repeat;
+ overflow: hidden;
+}
+/*** select ***/
+.wPaint-menu-select-holder{
+ position: absolute;
+ left: 1px;
+ z-index: 10;
+ overflow: hidden;
+}
+.wPaint-menu-select {
+ position: relative;
+ text-align: center;
+ overflow-y: scroll;
+ z-index: 100;
+}
+.wPaint-menu-select-option.first {
+ border-top: 0px;
+}
+/*** alignment ***/
+.wPaint-menu-alignment-horizontal .wPaint-menu-icon {
+ display: inline-block;
+}
+.wPaint-menu-alignment-vertical .wPaint-menu-icon {
+ display: block;
+}
+/*** status ***/
+.wPaint-status {
+ position: absolute;
+ display: none;
+ right: 0px;
+ bottom: 0px;
+}
+/*** modal ***/
+.wPaint-modal-bg {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+}
+.wPaint-modal {
+ position: absolute;
+ display: inline-block;
+}
+.wPaint-modal-holder {
+ display: inline-block;
+ overflow: hidden;
+}
+.wPaint-modal-content {
+ overflow-y: scroll;
+ width: 100%;
+ height: 100%;
+}
+.wPaint-modal-close {
+ position: absolute;
+}
+/*** text input ***/
+.wPaint-text-input{
+ margin: 0px;
+ padding: 0px;
+ outline-width: 0;
+ word-wrap: break-word;
+ overflow: hidden;
+}
+/*** file load ***/
+.wPaint-modal-img-holder {
+ line-height: 0px;
+}
+.wPaint-modal-img {
+ display: inline-block;
+}
+
+/**********************************************************************
+ * Generic Appearance
+ *
+ * Probably don't need to change these styles but can overwrite
+ * whatever is necessary.
+ **********************************************************************/
+
+/*** menu ***/
+.wPaint-menu-holder {
+ border-style: solid;
+ border-width: 1px;
+ box-shadow:3px 3px 5px #555555;
+}
+.wPaint-menu-handle {
+ cursor: pointer;
+}
+.wPaint-menu-icon {
+ border-style: solid;
+ border-width: 1px;
+ cursor: pointer;
+}
+.wPaint-menu-icon.disabled {
+ cursor: default;
+}
+.wPaint-menu-icon.disabled .wPaint-menu-icon-img {
+ opacity: 0.3;
+}
+.wPaint-menu-icon-img {
+ font-family: verdana;
+ font-weight: bold;
+ text-align: center;
+}
+/*** select ***/
+.wPaint-menu-select-holder {
+ border-style: solid;
+ border-width: 1px;
+ box-shadow: 1px 1px 2px #666;
+}
+.wPaint-menu-select {
+ font-family: verdana;
+ text-align: center;
+}
+.wPaint-menu-select-option {
+ border-top-style: solid;
+ border-top-width: 1px;
+ cursor: pointer;
+}
+.wPaint-menu-icon-select-img {
+ background-repeat: no-repeat;
+}
+.wPaint-menu-icon-group-arrow {
+ position: absolute;
+ right: 1px;
+ bottom: 1px;
+}
+/*** alignment ***/
+.wPaint-menu-alignment-horizontal .wPaint-menu-handle {
+ border-right-style: solid;
+ border-right-width: 1px;
+}
+.wPaint-menu-alignment-vertical .wPaint-menu-handle {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+}
+/*** status ***/
+.wPaint-status {
+ font-size: 10px;
+ font-family: verdana;
+ line-height: 10px;
+ height: 10px;
+ background-color: #3a3a3a;
+ color: #f0f0f0;
+ padding: 5px;
+ opacity: 0.5;
+}
+/*** modal ***/
+.wPaint-modal-bg {
+ background-color: #3a3a3a;
+ opacity: 0.8;
+}
+.wPaint-modal-holder {
+ height: 100px;
+ box-shadow: 3px 3px 5px #555555;
+ border-radius: 5px;
+ border-style: solid;
+ border-width: 2px;
+ cursor: default;
+}
+.wPaint-modal-close {
+ right: -7px;
+ top: -7px;
+ border-radius: 10px;
+ font-size: 8px;
+ line-height: 14px;
+ padding: 0 4px;
+ font-weight:bold;
+ border-style: solid;
+ border-width: 2px;
+ cursor: pointer;
+}
+/*** text input ***/
+.wPaint-text-input{
+ border: dotted #0000FF 1px;
+ background: none;
+}
+/*** file load ***/
+.wPaint-modal-img-holder {
+ border: solid #333 1px;
+ border-radius: 5px;
+ margin: 3px;
+ padding: 2px;
+ cursor: pointer;
+}
+.wPaint-modal-img {
+ width: 100px;
+ border-radius: 4px;
+ margin-bottom: 0px;
+}
+
+/**********************************************************************
+ * Size - standard theme
+ **********************************************************************/
+
+/*** menu ***/
+.wPaint-theme-standard .wPaint-menu-holder {
+ border-radius: 7px;
+}
+.wPaint-theme-standard .wPaint-menu-select-holder {
+ border-radius: 5px;
+}
+.wPaint-theme-standard .wPaint-menu-icon {
+ border-radius: 7px;
+}
+.wPaint-theme-standard .wPaint-menu-icon-img {
+ margin: 6px 5px 5px 6px;
+ width: 18px;
+ height: 18px;
+ line-height: 18px;
+ font-size: 12px;
+}
+.wPaint-theme-standard .wPaint-menu-colorpicker .wPaint-menu-icon-img {
+ margin: 3px 2px 2px 3px;
+ width: 24px;
+ height: 24px;
+ border-radius: 5px;
+}
+/*** select ***/
+.wPaint-theme-standard .wPaint-menu-icon-group .wPaint-menu-select-option {
+ padding: 4px;
+}
+.wPaint-theme-standard .wPaint-menu-icon-group-arrow {
+ width: 5px;
+ height: 3px;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAADCAYAAABbNsX4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK6wAACusBgosNWgAAABZ0RVh0Q3JlYXRpb24gVGltZQAwOC8xMS8xMyj8hykAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAKElEQVQImV3IwQ0AMAyDQKerspZ3pa9IVXmdGMB8nbbzjrYTNWoA1xeQ3RPyxUyE/gAAAABJRU5ErkJggg==');
+}
+.wPaint-theme-standard .wPaint-menu-select {
+ line-height: 10px;
+ font-size: 10px;
+ max-height: 136px;
+}
+.wPaint-theme-standard .wPaint-menu-select-option {
+ max-width: 50px;
+ padding: 4px 7px;
+}
+.wPaint-theme-standard .wPaint-menu-icon-select-img {
+ width: 18px;
+ height: 18px;
+}
+/* horizontal */
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal.wPaint-menu-nohandle .wPaint-menu-holder {
+ padding-left: 4px;
+}
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal .wPaint-menu-icon {
+ margin: 4px 5px 4px 0;
+}
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal .wPaint-menu-handle {
+ width: 30px;
+ height: 39px;
+ margin-right: 5px;
+ border-top-left-radius: 7px;
+ border-bottom-left-radius: 7px;
+}
+/* vertical */
+.wPaint-theme-standard .wPaint-menu-alignment-vertical.wPaint-menu-nohandle .wPaint-menu-holder {
+ padding-top: 4px;
+}
+.wPaint-theme-standard .wPaint-menu-alignment-vertical .wPaint-menu-icon {
+ margin: 0 4px 5px 4px;
+}
+.wPaint-theme-standard .wPaint-menu-alignment-vertical .wPaint-menu-handle {
+ width: 39px;
+ height: 30px;
+ margin-bottom: 5px;
+ border-top-left-radius: 7px;
+ border-top-right-radius: 7px;
+}
+
+/**********************************************************************
+ * Style - classic theme
+ **********************************************************************/
+
+/*** menu ***/
+.wPaint-theme-classic .wPaint-menu-holder {
+ border-color: #dadada;
+ background-color: #f0f0f0;
+}
+.wPaint-theme-classic .wPaint-menu-handle {
+ background-color: #dadada;
+ box-shadow: inset 1px 1px 3px #FFF;
+ border-color: #dadada;
+}
+.wPaint-theme-classic .wPaint-menu-icon {
+ border-color: #b9b9b9;
+ background-color: #b9b9b9;
+ box-shadow: inset 2px 2px 3px #eee, 1px 1px 2px #666;
+}
+.wPaint-theme-classic .wPaint-menu-icon.hover,
+.wPaint-theme-classic .wPaint-menu-icon.active {
+ border-color: #99ccff;
+ background-color: #aaccff;
+}
+.wPaint-theme-classic .wPaint-menu-icon-img {
+ color: #696969;
+}
+/*** select ***/
+.wPaint-theme-classic .wPaint-menu-select-holder {
+ border-color: #CACACA;
+}
+.wPaint-theme-classic .wPaint-menu-select {
+ color: #494949;
+}
+.wPaint-theme-classic .wPaint-menu-select-option {
+ box-shadow: inset 2px 2px 3px #fff;
+ border-top-color: #CACACA;
+ background-color: #F0F0F0;
+}
+.wPaint-theme-classic .wPaint-menu-select-option:hover {
+ box-shadow: inset 1px 1px 1px #fff;
+ background-color: #99ccff;
+ color: #f0f0f0;
+}
+/*** modal ***/
+.wPaint-theme-classic .wPaint-modal-close,
+.wPaint-theme-classic .wPaint-modal-holder {
+ border-color: #3a3a3a;
+ background-color: #f0f0f0;
+}
diff --git a/static/js/wpaint/src/wPaint.js b/static/js/wpaint/src/wPaint.js
new file mode 100644
index 0000000..9717f6d
--- /dev/null
+++ b/static/js/wpaint/src/wPaint.js
@@ -0,0 +1,1181 @@
+(function ($) {
+ 'use strict';
+
+ /************************************************************************
+ * Paint class
+ ************************************************************************/
+ function Paint(el, options) {
+ this.$el = $(el);
+ this.options = options;
+ this.init = false;
+
+ this.menus = {primary: null, active: null, all: {}};
+ this.previousMode = null;
+ this.width = this.$el.width();
+ this.height = this.$el.height();
+
+ this.ctxBgResize = false;
+ this.ctxResize = false;
+
+ this.generate();
+ this._init();
+ }
+
+ Paint.prototype = {
+ generate: function () {
+ if (this.init) { return this; }
+
+ var _this = this;
+
+ // automatically appends each canvas
+ // also returns the jQuery object so we can chain events right off the function call.
+ // for the tempCanvas we will be setting some extra attributes but don't won't matter
+ // as they will be reset on mousedown anyway.
+ function createCanvas(name) {
+ var newName = (name ? name.capitalize() : ''),
+ canvasName = 'canvas' + newName,
+ ctxName = 'ctx' + newName;
+
+ _this[canvasName] = document.createElement('canvas');
+ _this[ctxName] = _this[canvasName].getContext('2d');
+ _this['$' + canvasName] = $(_this[canvasName]);
+
+ _this['$' + canvasName]
+ .attr('class', 'wPaint-canvas' + (name ? '-' + name : ''))
+ .attr('width', _this.width + 'px')
+ .attr('height', _this.height + 'px')
+ .css({position: 'absolute', left: 0, top: 0});
+
+ _this.$el.append(_this['$' + canvasName]);
+
+ return _this['$' + canvasName];
+ }
+
+ // event functions
+ function canvasMousedown(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ _this.draw = true;
+ e.canvasEvent = 'down';
+ _this._closeSelectBoxes();
+ _this._callShapeFunc.apply(_this, [e]);
+ }
+
+ function documentMousemove(e) {
+ if (_this.draw) {
+ e.canvasEvent = 'move';
+ _this._callShapeFunc.apply(_this, [e]);
+ }
+ }
+
+ function documentMouseup(e) {
+
+ //make sure we are in draw mode otherwise this will fire on any mouse up.
+ if (_this.draw) {
+ _this.draw = false;
+ e.canvasEvent = 'up';
+ _this._callShapeFunc.apply(_this, [e]);
+ }
+ }
+
+ // create bg canvases
+ createCanvas('bg');
+
+ // create drawing canvas
+ createCanvas('')
+ .on('mousedown', canvasMousedown)
+ .bindMobileEvents();
+
+ // create temp canvas for drawing shapes temporarily
+ // before transfering to main canvas
+ createCanvas('temp').hide();
+
+ // event handlers for drawing
+ $(document)
+ .on('mousemove', documentMousemove)
+ .on('mousedown', $.proxy(this._closeSelectBoxes, this))
+ .on('mouseup', documentMouseup);
+
+ // we will need to preset theme to get proper dimensions
+ // when creating menus which will be appended after this
+ this.setTheme(this.options.theme);
+ },
+
+ _init: function () {
+ var index = null,
+ setFuncName = null;
+
+ this.init = true;
+
+ // run any set functions if they exist
+ for (index in this.options) {
+ setFuncName = 'set' + index.capitalize();
+ if (this[setFuncName]) { this[setFuncName](this.options[index]); }
+ }
+
+ // fix menus
+ this._fixMenus();
+
+ // initialize active menu button
+ this.menus.primary._getIcon(this.options.mode).trigger('click');
+ },
+
+ resize: function () {
+ var bg = this.getBg(),
+ image = this.getImage();
+
+ this.width = this.$el.width();
+ this.height = this.$el.height();
+
+ this.canvasBg.width = this.width;
+ this.canvasBg.height = this.height;
+ this.canvas.width = this.width;
+ this.canvas.height = this.height;
+
+ if (this.ctxBgResize === false) {
+ this.ctxBgResize = true;
+ this.setBg(bg, true);
+ }
+
+ if (this.ctxResize === false) {
+ this.ctxResize = true;
+ this.setImage(image, '', true, true);
+ }
+ },
+
+ /************************************
+ * setters
+ ************************************/
+ setTheme: function (theme) {
+ var i, ii;
+
+ theme = theme.split(' ');
+
+ // remove anything beginning with "wPaint-theme-" first
+ this.$el.attr('class', (this.$el.attr('class') || '').replace(/wPaint-theme-.+\s|wPaint-theme-.+$/, ''));
+
+ // add each theme
+ for (i = 0, ii = theme.length; i < ii; i++) {
+ this.$el.addClass('wPaint-theme-' + theme[i]);
+ }
+ },
+
+ setMode: function (mode) {
+ this.setCursor(mode);
+ this.previousMode = this.options.mode;
+ this.options.mode = mode;
+ },
+
+ setImage: function (img, ctxType, resize, notUndo) {
+ if (!img) { return true; }
+
+ var _this = this,
+ myImage = null,
+ ctx = '';
+
+ function loadImage() {
+ var ratio = 1, xR = 0, yR = 0, x = 0, y = 0, w = myImage.width, h = myImage.height;
+
+ if (!resize) {
+ // get width/height
+ if (myImage.width > _this.width || myImage.height > _this.height || _this.options.imageStretch) {
+ xR = _this.width / myImage.width;
+ yR = _this.height / myImage.height;
+
+ ratio = xR < yR ? xR : yR;
+
+ w = myImage.width * ratio;
+ h = myImage.height * ratio;
+ }
+
+ // get left/top (centering)
+ x = (_this.width - w) / 2;
+ y = (_this.height - h) / 2;
+ }
+
+ ctx.clearRect(0, 0, _this.width, _this.height);
+ ctx.drawImage(myImage, x, y, w, h);
+
+ _this[ctxType + 'Resize'] = false;
+
+ // Default is to run the undo.
+ // If it's not to be run set it the flag to true.
+ if (!notUndo) {
+ _this._addUndo();
+ }
+ }
+
+ ctxType = 'ctx' + (ctxType || '').capitalize();
+ ctx = this[ctxType];
+
+ if (window.rgbHex(img)) {
+ ctx.clearRect(0, 0, this.width, this.height);
+ ctx.fillStyle = img;
+ ctx.rect(0, 0, this.width, this.height);
+ ctx.fill();
+ }
+ else {
+ myImage = new Image();
+ myImage.src = img.toString();
+ $(myImage).load(loadImage);
+ }
+ },
+
+ setBg: function (img, resize) {
+ if (!img) { return true; }
+
+ this.setImage(img, 'bg', resize, true);
+ },
+
+ setCursor: function (cursor) {
+ cursor = $.fn.wPaint.cursors[cursor] || $.fn.wPaint.cursors['default'];
+
+ this.$el.css('cursor', 'url("' + this.options.path + cursor.path + '") ' + cursor.left + ' ' + cursor.top + ', default');
+ },
+
+ setMenuOrientation: function (orientation) {
+ $.each(this.menus.all, function (i, menu) {
+ menu.options.aligment = orientation;
+ menu.setAlignment(orientation);
+ });
+ },
+
+ getImage: function (withBg) {
+ var canvasSave = document.createElement('canvas'),
+ ctxSave = canvasSave.getContext('2d');
+
+ withBg = withBg === false ? false : true;
+
+ $(canvasSave)
+ .css({display: 'none', position: 'absolute', left: 0, top: 0})
+ .attr('width', this.width)
+ .attr('height', this.height);
+
+ if (withBg) { ctxSave.drawImage(this.canvasBg, 0, 0); }
+ ctxSave.drawImage(this.canvas, 0, 0);
+
+ return canvasSave.toDataURL();
+ },
+
+ getBg: function () {
+ return this.canvasBg.toDataURL();
+ },
+
+ /************************************
+ * prompts
+ ************************************/
+ _displayStatus: function (msg) {
+ var _this = this;
+
+ if (!this.$status) {
+ this.$status = $('<div class="wPaint-status"></div>');
+ this.$el.append(this.$status);
+ }
+
+ this.$status.html(msg);
+ clearTimeout(this.displayStatusTimer);
+
+ this.$status.fadeIn(500, function () {
+ _this.displayStatusTimer = setTimeout(function () { _this.$status.fadeOut(500); }, 1500);
+ });
+ },
+
+ _showModal: function ($content) {
+ var _this = this,
+ $bg = this.$el.children('.wPaint-modal-bg'),
+ $modal = this.$el.children('.wPaint-modal');
+
+ function modalFadeOut() {
+ $bg.remove();
+ $modal.remove();
+ _this._createModal($content);
+ }
+
+ if ($bg.length) {
+ $modal.fadeOut(500, modalFadeOut);
+ }
+ else {
+ this._createModal($content);
+ }
+ },
+
+ _createModal: function ($content) {
+ $content = $('<div class="wPaint-modal-content"></div>').append($content.children());
+
+ var $bg = $('<div class="wPaint-modal-bg"></div>'),
+ $modal = $('<div class="wPaint-modal"></div>'),
+ $holder = $('<div class="wPaint-modal-holder"></div>'),
+ $close = $('<div class="wPaint-modal-close">X</div>');
+
+ function modalClick() {
+ $modal.fadeOut(500, modalFadeOut);
+ }
+
+ function modalFadeOut() {
+ $bg.remove();
+ $modal.remove();
+ }
+
+ $close.on('click', modalClick);
+ $modal.append($holder.append($content)).append($close);
+ this.$el.append($bg).append($modal);
+
+ $modal.css({
+ left: (this.$el.outerWidth() / 2) - ($modal.outerWidth(true) / 2),
+ top: (this.$el.outerHeight() / 2) - ($modal.outerHeight(true) / 2)
+ });
+
+ $modal.fadeIn(500);
+ },
+
+ /************************************
+ * menu helpers
+ ************************************/
+ _createMenu: function (name, options) {
+ options = options || {};
+ options.alignment = this.options.menuOrientation;
+ options.handle = this.options.menuHandle;
+
+ return new Menu(this, name, options);
+ },
+
+ _fixMenus: function () {
+ var _this = this,
+ $selectHolder = null;
+
+ function selectEach(i, el) {
+ var $el = $(el),
+ $select = $el.clone();
+
+ $select.appendTo(_this.$el);
+
+ if ($select.outerHeight() === $select.get(0).scrollHeight) {
+ $el.css({overflowY: 'auto'});
+ }
+
+ $select.remove();
+ }
+
+ // TODO: would be nice to do this better way
+ // for some reason when setting overflowY:auto with dynamic content makes the width act up
+ for (var key in this.menus.all) {
+ $selectHolder = _this.menus.all[key].$menu.find('.wPaint-menu-select-holder');
+ if ($selectHolder.length) { $selectHolder.children().each(selectEach); }
+ }
+ },
+
+ _closeSelectBoxes: function (item) {
+ var key, $selectBoxes;
+
+ for (key in this.menus.all) {
+ $selectBoxes = this.menus.all[key].$menuHolder.children('.wPaint-menu-icon-select');
+
+ // hide any open select menus excluding the current menu
+ // this is to avoid the double toggle since there are some
+ // other events running here
+ if (item) { $selectBoxes = $selectBoxes.not('.wPaint-menu-icon-name-' + item.name); }
+
+ $selectBoxes.children('.wPaint-menu-select-holder').hide();
+ }
+ },
+
+ /************************************
+ * events
+ ************************************/
+ //_imageOnload: function () {
+ // /* a blank helper function for post image load calls on canvas - can be extended by other plugins using the setImage called */
+ //},
+
+ _callShapeFunc: function (e) {
+
+ // TODO: this is where issues with mobile offsets are probably off
+ var canvasOffset = this.$canvas.offset(),
+ canvasEvent = e.canvasEvent.capitalize(),
+ func = '_draw' + this.options.mode.capitalize() + canvasEvent;
+
+ // update offsets here since we are detecting mouseup on $(document) not on the canvas
+ e.pageX = Math.floor(e.pageX - canvasOffset.left);
+ e.pageY = Math.floor(e.pageY - canvasOffset.top);
+
+ // call drawing func
+ if (this[func]) { this[func].apply(this, [e]); }
+
+ // run callback if set
+ if (this.options['draw' + canvasEvent]) { this.options['_draw' + canvasEvent].apply(this, [e]); }
+
+ // run options (user) callback if set
+ if (canvasEvent === 'Down' && this.options.onShapeDown) { this.options.onShapeDown.apply(this, [e]); }
+ else if (canvasEvent === 'Move' && this.options.onShapeMove) { this.options.onShapeMove.apply(this, [e]); }
+ else if (canvasEvent === 'Up' && this.options.onShapeUp) { this.options.onShapeUp.apply(this, [e]); }
+ },
+
+ _stopPropagation: function (e) {
+ e.stopPropagation();
+ },
+
+ /************************************
+ * shape helpers
+ ************************************/
+ _drawShapeDown: function (e) {
+ this.$canvasTemp
+ .css({left: e.PageX, top: e.PageY})
+ .attr('width', 0)
+ .attr('height', 0)
+ .show();
+
+ this.canvasTempLeftOriginal = e.pageX;
+ this.canvasTempTopOriginal = e.pageY;
+ },
+
+ _drawShapeMove: function (e, factor) {
+ var xo = this.canvasTempLeftOriginal,
+ yo = this.canvasTempTopOriginal;
+
+ // we may need these in other funcs, so we'll just pass them along with the event
+ factor = factor || 2;
+ e.left = (e.pageX < xo ? e.pageX : xo);
+ e.top = (e.pageY < yo ? e.pageY : yo);
+ e.width = Math.abs(e.pageX - xo);
+ e.height = Math.abs(e.pageY - yo);
+ e.x = this.options.lineWidth / 2 * factor;
+ e.y = this.options.lineWidth / 2 * factor;
+ e.w = e.width - this.options.lineWidth * factor;
+ e.h = e.height - this.options.lineWidth * factor;
+
+ $(this.canvasTemp)
+ .css({left: e.left, top: e.top})
+ .attr('width', e.width)
+ .attr('height', e.height);
+
+ // store these for later to use in our "up" call
+ this.canvasTempLeftNew = e.left;
+ this.canvasTempTopNew = e.top;
+
+ factor = factor || 2;
+
+ // TODO: set this globally in _drawShapeDown (for some reason colors are being reset due to canvas resize - is there way to permanently set it)
+ this.ctxTemp.fillStyle = this.options.fillStyle;
+ this.ctxTemp.strokeStyle = this.options.strokeStyle;
+ this.ctxTemp.lineWidth = this.options.lineWidth * factor;
+ },
+
+ _drawShapeUp: function () {
+ this.ctx.drawImage(this.canvasTemp, this.canvasTempLeftNew, this.canvasTempTopNew);
+ this.$canvasTemp.hide();
+ },
+
+ /****************************************
+ * dropper
+ ****************************************/
+ _drawDropperDown: function (e) {
+ var pos = {x: e.pageX, y: e.pageY},
+ pixel = this._getPixel(this.ctx, pos),
+ color = null;
+
+ // if we get no color try getting from the background
+ //if(pixel.r === 0 && pixel.g === 0 && pixel.b === 0 && pixel.a === 0) {
+ // imageData = this.ctxBg.getImageData(0, 0, this.width, this.height)
+ // pixel = this._getPixel(imageData, pos);
+ //}
+
+ color = 'rgba(' + [ pixel.r, pixel.g, pixel.b, pixel.a ].join(',') + ')';
+
+ // set color from dropper here
+ this.options[this.dropper] = color;
+ this.menus.active._getIcon(this.dropper).wColorPicker('color', color);
+ },
+
+ _drawDropperUp: function () {
+ this.setMode(this.previousMode);
+ },
+
+ // get pixel data represented as RGBa color from pixel array.
+ _getPixel: function (ctx, pos) {
+ var imageData = ctx.getImageData(0, 0, this.width, this.height),
+ pixelArray = imageData.data,
+ base = ((pos.y * imageData.width) + pos.x) * 4;
+
+ return {
+ r: pixelArray[base],
+ g: pixelArray[base + 1],
+ b: pixelArray[base + 2],
+ a: pixelArray[base + 3]
+ };
+ }
+ };
+
+ /************************************************************************
+ * Menu class
+ ************************************************************************/
+ function Menu(wPaint, name, options) {
+ this.wPaint = wPaint;
+ this.options = options;
+ this.name = name;
+ this.type = !wPaint.menus.primary ? 'primary' : 'secondary';
+ this.docked = true;
+ this.dockOffset = {left: 0, top: 0};
+
+ this.generate();
+ }
+
+ Menu.prototype = {
+ generate: function () {
+ this.$menu = $('<div class="wPaint-menu"></div>');
+ this.$menuHolder = $('<div class="wPaint-menu-holder wPaint-menu-name-' + this.name + '"></div>');
+
+ if (this.options.handle) { this.$menuHandle = this._createHandle(); }
+ else { this.$menu.addClass('wPaint-menu-nohandle'); }
+
+ if (this.type === 'primary') {
+
+ // store the primary menu in primary object - we will need this reference later
+ this.wPaint.menus.primary = this;
+
+ this.setOffsetLeft(this.options.offsetLeft);
+ this.setOffsetTop(this.options.offsetTop);
+ }
+ else if (this.type === 'secondary') {
+ this.$menu.hide();
+ }
+
+ // append menu items
+ this.$menu.append(this.$menuHolder.append(this.$menuHandle));
+ this.reset();
+
+ // append menu
+ this.wPaint.$el.append(this.$menu);
+
+ this.setAlignment(this.options.alignment);
+ },
+
+ // create / reset menu - will add new entries in the array
+ reset: function () {
+ var _this = this,
+ menu = $.fn.wPaint.menus[this.name],
+ key;
+
+ // self invoking function
+ function itemAppend(item) { _this._appendItem(item); }
+
+ for (key in menu.items) {
+
+ // only add unique (new) items (icons)
+ if (!this.$menuHolder.children('.wPaint-menu-icon-name-' + key).length) {
+
+ // add the item name, we will need this internally
+ menu.items[key].name = key;
+
+ // use default img if img not set
+ menu.items[key].img = _this.wPaint.options.path + (menu.items[key].img || menu.img);
+
+ // make self invoking to avoid overwrites
+ (itemAppend)(menu.items[key]);
+ }
+ }
+ },
+
+ _appendItem: function (item) {
+ var $item = this['_createIcon' + item.icon.capitalize()](item);
+
+ if (item.after) {
+ this.$menuHolder.children('.wPaint-menu-icon-name-' + item.after).after($item);
+ }
+ else {
+ this.$menuHolder.append($item);
+ }
+ },
+
+ /************************************
+ * setters
+ ************************************/
+ setOffsetLeft: function (left) {
+ this.$menu.css({left: left});
+ },
+
+ setOffsetTop: function (top) {
+ this.$menu.css({top: top});
+ },
+
+ setAlignment: function (alignment) {
+ var tempLeft = this.$menu.css('left');
+
+ this.$menu.attr('class', this.$menu.attr('class').replace(/wPaint-menu-alignment-.+\s|wPaint-menu-alignment-.+$/, ''));
+ this.$menu.addClass('wPaint-menu-alignment-' + alignment);
+
+ this.$menu.width('auto').css('left', -10000);
+ this.$menu.width(this.$menu.width()).css('left', tempLeft);
+
+ // set proper offsets based on alignment
+ if (this.type === 'secondary') {
+ if (this.options.alignment === 'horizontal') {
+ this.dockOffset.top = this.wPaint.menus.primary.$menu.outerHeight(true);
+ }
+ else {
+ this.dockOffset.left = this.wPaint.menus.primary.$menu.outerWidth(true);
+ }
+ }
+ },
+
+ /************************************
+ * handle
+ ************************************/
+ _createHandle: function () {
+ var _this = this,
+ $handle = $('<div class="wPaint-menu-handle"></div>');
+
+ // draggable functions
+ function draggableStart() {
+ _this.docked = false;
+ _this._setDrag();
+ }
+
+ function draggableStop() {
+ $.each(_this.$menu.data('ui-draggable').snapElements, function (i, el) {
+ var offset = _this.$menu.offset(),
+ offsetPrimary = _this.wPaint.menus.primary.$menu.offset();
+
+ _this.dockOffset.left = offset.left - offsetPrimary.left;
+ _this.dockOffset.top = offset.top - offsetPrimary.top;
+ _this.docked = el.snapping;
+ });
+
+ _this._setDrag();
+ }
+
+ function draggableDrag() {
+ _this._setIndex();
+ }
+
+ // the drag/snap events for menus are tricky
+ // init handle for ALL menus, primary menu will drag a secondary menu with it, but that is un/binded in the toggle function
+ this.$menu.draggable({handle: $handle});
+
+ // if it's a secondary menu we want to check for snapping
+ // on drag we set docked to false, on snap we set it back to true
+ if (this.type === 'secondary') {
+ this.$menu.draggable('option', 'snap', this.wPaint.menus.primary.$menu);
+ this.$menu.draggable('option', 'start', draggableStart);
+ this.$menu.draggable('option', 'stop', draggableStop);
+ this.$menu.draggable('option', 'drag', draggableDrag);
+ }
+
+ $handle.bindMobileEvents();
+
+ return $handle;
+ },
+
+ /************************************
+ * generic icon
+ ************************************/
+ _createIconBase: function (item) {
+ var _this = this,
+ $icon = $('<div class="wPaint-menu-icon wPaint-menu-icon-name-' + item.name + '"></div>'),
+ $iconImg = $('<div class="wPaint-menu-icon-img"></div>'),
+ width = $iconImg.realWidth(null, null, this.wPaint.$el);
+
+ function mouseenter(e) {
+ var $el = $(e.currentTarget);
+
+ $el.siblings('.hover').removeClass('hover');
+ if (!$el.hasClass('disabled')) { $el.addClass('hover'); }
+ }
+
+ function mouseleave(e) {
+ $(e.currentTarget).removeClass('hover');
+ }
+
+ function click() {
+ _this.wPaint.menus.active = _this;
+ }
+
+ $icon
+ .attr('title', item.title)
+ .on('mousedown', $.proxy(this.wPaint._closeSelectBoxes, this.wPaint, item))
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', mouseleave)
+ .on('click', click);
+
+ // can have index:0 so be careful here
+ if ($.isNumeric(item.index)) {
+ $iconImg
+ .css({
+ backgroundImage: 'url(' + item.img + ')',
+ backgroundPosition: (-width * item.index) + 'px 0px'
+ });
+ }
+
+ return $icon.append($iconImg);
+ },
+
+ /************************************
+ * icon group
+ ************************************/
+ _createIconGroup: function (item) {
+ var _this = this,
+ css = {backgroundImage: 'url(' + item.img + ')'},
+ $icon = this.$menuHolder.children('.wPaint-menu-icon-group-' + item.group),
+ iconExists = $icon.length,
+ $selectHolder = null,
+ $option = null,
+ $item = null,
+ width = 0;
+
+ // local functions
+ function setIconClick() {
+
+ // only trigger if menu is not visible otherwise it will fire twice
+ // from the mousedown to open the menu which we want just to display the menu
+ // not fire the button callback
+ if (!$icon.children('.wPaint-menu-select-holder').is(':visible')) {
+ item.callback.apply(_this.wPaint, []);
+ }
+ }
+
+ function selectHolderClick() {
+ $icon.addClass('active').siblings('.active').removeClass('active');
+ }
+
+ function optionClick() {
+
+ // rebind the main icon when we select an option
+ $icon
+ .attr('title', item.title)
+ .off('click.setIcon')
+ .on('click.setIcon', setIconClick);
+
+ // run the callback right away when we select an option
+ $icon.children('.wPaint-menu-icon-img').css(css);
+ item.callback.apply(_this.wPaint, []);
+ }
+
+ // crate icon if it doesn't exist yet
+ if (!iconExists) {
+ $icon = this._createIconBase(item)
+ .addClass('wPaint-menu-icon-group wPaint-menu-icon-group-' + item.group)
+ .on('click.setIcon', setIconClick)
+ .on('mousedown', $.proxy(this._iconClick, this));
+ }
+
+ // get the proper width here now that we have the icon
+ // this is for the select box group not the main icon
+ width = $icon.children('.wPaint-menu-icon-img').realWidth(null, null, this.wPaint.$el);
+ css.backgroundPosition = (-width * item.index) + 'px center';
+
+ // create selectHolder if it doesn't exist
+ $selectHolder = $icon.children('.wPaint-menu-select-holder');
+ if (!$selectHolder.length) {
+ $selectHolder = this._createSelectBox($icon);
+ $selectHolder.children().on('click', selectHolderClick);
+ }
+
+ $item = $('<div class="wPaint-menu-icon-select-img"></div>')
+ .attr('title', item.title)
+ .css(css);
+
+ $option = this._createSelectOption($selectHolder, $item)
+ .addClass('wPaint-menu-icon-name-' + item.name)
+ .on('click', optionClick);
+
+ // move select option into place if after is set
+ if (item.after) {
+ $selectHolder.children('.wPaint-menu-select').children('.wPaint-menu-icon-name-' + item.after).after($option);
+ }
+
+ // we only want to return an icon to append on the first run of a group
+ if (!iconExists) { return $icon; }
+ },
+
+ /************************************
+ * icon generic
+ ************************************/
+ _createIconGeneric: function (item) {
+
+ // just a go between for the iconGeneric type
+ return this._createIconActivate(item);
+ },
+
+ /************************************
+ * icon
+ ************************************/
+ _createIconActivate: function (item) {
+
+ // since we are piggy backing icon with the item.group
+ // we'll just do a redirect and keep the code separate for group icons
+ if (item.group) { return this._createIconGroup(item); }
+
+ var _this = this,
+ $icon = this._createIconBase(item);
+
+ function iconClick(e) {
+ if (item.icon !== 'generic') { _this._iconClick(e); }
+ item.callback.apply(_this.wPaint, [e]);
+ }
+
+ $icon.on('click', iconClick);
+
+ return $icon;
+ },
+
+ _isIconDisabled: function (name) {
+ return this.$menuHolder.children('.wPaint-menu-icon-name-' + name).hasClass('disabled');
+ },
+
+ _setIconDisabled: function (name, disabled) {
+ var $icon = this.$menuHolder.children('.wPaint-menu-icon-name-' + name);
+
+ if (disabled) {
+ $icon.addClass('disabled').removeClass('hover');
+ }
+ else {
+ $icon.removeClass('disabled');
+ }
+ },
+
+ _getIcon: function (name) {
+ return this.$menuHolder.children('.wPaint-menu-icon-name-' + name);
+ },
+
+ _iconClick: function (e) {
+ var $el = $(e.currentTarget),
+ menus = this.wPaint.menus.all;
+
+ // make sure to loop using parent object - don't use .wPaint-menu-secondary otherwise we would hide menu for all canvases
+ for (var menu in menus) {
+ if (menus[menu] && menus[menu].type === 'secondary') { menus[menu].$menu.hide(); }
+ }
+
+ $el.siblings('.active').removeClass('active');
+ if (!$el.hasClass('disabled')) { $el.addClass('active'); }
+ },
+
+ /************************************
+ * iconToggle
+ ************************************/
+ _createIconToggle: function (item) {
+ var _this = this,
+ $icon = this._createIconBase(item);
+
+ function iconClick() {
+ $icon.toggleClass('active');
+ item.callback.apply(_this.wPaint, [$icon.hasClass('active')]);
+ }
+
+ $icon.on('click', iconClick);
+
+ return $icon;
+ },
+
+ /************************************
+ * select
+ ************************************/
+ _createIconSelect: function (item) {
+ var _this = this,
+ $icon = this._createIconBase(item),
+ $selectHolder = this._createSelectBox($icon),
+ i, ii, $option;
+
+ function optionClick(e) {
+ $icon.children('.wPaint-menu-icon-img').html($(e.currentTarget).html());
+ item.callback.apply(_this.wPaint, [$(e.currentTarget).html()]);
+ }
+
+ // add values for select
+ for (i = 0, ii = item.range.length; i < ii; i++) {
+ $option = this._createSelectOption($selectHolder, item.range[i]);
+ $option.on('click', optionClick);
+ if (item.useRange) { $option.css(item.name, item.range[i]); }
+ }
+
+ return $icon;
+ },
+
+ _createSelectBox: function ($icon) {
+ var $selectHolder = $('<div class="wPaint-menu-select-holder"></div>'),
+ $select = $('<div class="wPaint-menu-select"></div>'),
+ timer = null;
+
+ function clickSelectHolder(e) {
+ e.stopPropagation();
+ $selectHolder.hide();
+ }
+
+ function iconMousedown() {
+ timer = setTimeout(function () { $selectHolder.toggle(); }, 200);
+ }
+
+ function iconMouseup() {
+ clearTimeout(timer);
+ }
+
+ function iconClick() {
+ $selectHolder.toggle();
+ }
+
+ $selectHolder
+ .on('mousedown mouseup', this.wPaint._stopPropagation)
+ .on('click', clickSelectHolder)
+ .hide();
+
+ // of hozizontal we'll pop below the icon
+ if (this.options.alignment === 'horizontal') {
+ $selectHolder.css({left: 0, top: $icon.children('.wPaint-menu-icon-img').realHeight('outer', true, this.wPaint.$el)});
+ }
+ // vertical we'll pop to the right
+ else {
+ $selectHolder.css({left: $icon.children('.wPaint-menu-icon-img').realWidth('outer', true, this.wPaint.$el), top: 0});
+ }
+
+ $icon
+ .addClass('wPaint-menu-icon-select')
+ .append('<div class="wPaint-menu-icon-group-arrow"></div>')
+ .append($selectHolder.append($select));
+
+ // for groups we want to add a delay before the selectBox pops up
+ if ($icon.hasClass('wPaint-menu-icon-group')) {
+ $icon
+ .on('mousedown', iconMousedown)
+ .on('mouseup', iconMouseup);
+ }
+ else { $icon.on('click', iconClick); }
+
+ return $selectHolder;
+ },
+
+ _createSelectOption: function ($selectHolder, value) {
+ var $select = $selectHolder.children('.wPaint-menu-select'),
+ $option = $('<div class="wPaint-menu-select-option"></div>').append(value);
+
+ // set class for first item to remove any undesired styles like borders
+ if (!$select.children().length) { $option.addClass('first'); }
+
+ $select.append($option);
+
+ return $option;
+ },
+
+ _setSelectValue: function (icon, value) {
+ this._getIcon(icon).children('.wPaint-menu-icon-img').html(value);
+ },
+
+ /************************************
+ * color picker
+ ************************************/
+ _createIconColorPicker: function (item) {
+ var _this = this,
+ $icon = this._createIconBase(item);
+
+ function iconClick() {
+
+ // if we happen to click on this while in dropper mode just revert to previous
+ if (_this.wPaint.options.mode === 'dropper') { _this.wPaint.setMode(_this.wPaint.previousMode); }
+ }
+
+ function iconOnSelect(color) {
+ item.callback.apply(_this.wPaint, [color]);
+ }
+
+ function iconOnDropper() {
+ $icon.trigger('click');
+ _this.wPaint.dropper = item.name;
+ _this.wPaint.setMode('dropper');
+ }
+
+ $icon
+ .on('click', iconClick)
+ .addClass('wPaint-menu-colorpicker')
+ .wColorPicker({
+ mode: 'click',
+ generateButton: false,
+ dropperButton: true,
+ onSelect: iconOnSelect,
+ onDropper: iconOnDropper
+ });
+
+ return $icon;
+ },
+
+ _setColorPickerValue: function (icon, value) {
+ this._getIcon(icon).children('.wPaint-menu-icon-img').css('backgroundColor', value);
+ },
+
+ /************************************
+ * menu toggle
+ ************************************/
+ _createIconMenu: function (item) {
+ var _this = this,
+ $icon = this._createIconActivate(item);
+
+ function iconClick() {
+ _this.wPaint.setCursor(item.name);
+
+ // the items name here will be the menu name
+ var menu = _this.wPaint.menus.all[item.name];
+ menu.$menu.toggle();
+ if (_this.handle) {
+ menu._setDrag();
+ } else {
+ menu._setPosition();
+ }
+ }
+
+ $icon.on('click', iconClick);
+
+ return $icon;
+ },
+
+ // here we specify which menu will be dragged
+ _setDrag: function () {
+ var $menu = this.$menu,
+ drag = null, stop = null;
+
+ if ($menu.is(':visible')) {
+ if (this.docked) {
+
+ // make sure we are setting proper menu object here
+ drag = stop = $.proxy(this._setPosition, this);
+ this._setPosition();
+ }
+
+ // register drag/stop events
+ this.wPaint.menus.primary.$menu.draggable('option', 'drag', drag);
+ this.wPaint.menus.primary.$menu.draggable('option', 'stop', stop);
+ }
+ },
+
+ _setPosition: function () {
+ var offset = this.wPaint.menus.primary.$menu.position();
+
+ this.$menu.css({
+ left: offset.left + this.dockOffset.left,
+ top: offset.top + this.dockOffset.top
+ });
+ },
+
+ _setIndex: function () {
+ var primaryOffset = this.wPaint.menus.primary.$menu.offset(),
+ secondaryOffset = this.$menu.offset();
+
+ if (
+ secondaryOffset.top < primaryOffset.top ||
+ secondaryOffset.left < primaryOffset.left
+ ) {
+ this.$menu.addClass('wPaint-menu-behind');
+ }
+ else {
+ this.$menu.removeClass('wPaint-menu-behind');
+ }
+ }
+ };
+
+ /************************************************************************
+ * wPaint
+ ************************************************************************/
+ $.support.canvas = (document.createElement('canvas')).getContext;
+
+ $.fn.wPaint = function (options, value) {
+
+ function create() {
+ if (!$.support.canvas) {
+ $(this).html('Browser does not support HTML5 canvas, please upgrade to a more modern browser.');
+ return false;
+ }
+
+ return $.proxy(get, this)();
+ }
+
+ function get() {
+ var wPaint = $.data(this, 'wPaint');
+
+ if (!wPaint) {
+ wPaint = new Paint(this, $.extend(true, {}, options));
+ $.data(this, 'wPaint', wPaint);
+ }
+
+ return wPaint;
+ }
+
+ function runOpts() {
+ var wPaint = $.data(this, 'wPaint');
+
+ if (wPaint) {
+ if (wPaint[options]) { wPaint[options].apply(wPaint, [value]); }
+ else if (value !== undefined) {
+ if (wPaint[func]) { wPaint[func].apply(wPaint, [value]); }
+ if (wPaint.options[options]) { wPaint.options[options] = value; }
+ }
+ else {
+ if (wPaint[func]) { values.push(wPaint[func].apply(wPaint, [value])); }
+ else if (wPaint.options[options]) { values.push(wPaint.options[options]); }
+ else { values.push(undefined); }
+ }
+ }
+ }
+
+ if (typeof options === 'string') {
+ var values = [],
+ func = (value ? 'set' : 'get') + options.charAt(0).toUpperCase() + options.substring(1);
+
+ this.each(runOpts);
+
+ if (values.length) { return values.length === 1 ? values[0] : values; }
+
+ return this;
+ }
+
+ options = $.extend({}, $.fn.wPaint.defaults, options);
+ options.lineWidth = parseInt(options.lineWidth, 10);
+ options.fontSize = parseInt(options.fontSize, 10);
+
+ return this.each(create);
+ };
+
+ /************************************************************************
+ * extend
+ ************************************************************************/
+ $.fn.wPaint.extend = function (funcs, protoType) {
+ var key;
+
+ function elEach(func) {
+ if (protoType[func]) {
+ var tmpFunc = Paint.prototype[func],
+ newFunc = funcs[func];
+
+ protoType[func] = function () {
+ tmpFunc.apply(this, arguments);
+ newFunc.apply(this, arguments);
+ };
+ }
+ else {
+ protoType[func] = funcs[func];
+ }
+ }
+
+ protoType = protoType === 'menu' ? Menu.prototype : Paint.prototype;
+
+ for (key in funcs) { (elEach)(key); }
+ };
+
+ /************************************************************************
+ * Init holders
+ ************************************************************************/
+ $.fn.wPaint.menus = {};
+
+ $.fn.wPaint.cursors = {};
+
+ $.fn.wPaint.defaults = {
+ path: '/', // set absolute path for images and cursors
+ theme: 'standard classic', // set theme
+ autoScaleImage: true, // auto scale images to size of canvas (fg and bg)
+ autoCenterImage: true, // auto center images (fg and bg, default is left/top corner)
+ menuHandle: true, // setting to false will means menus cannot be dragged around
+ menuOrientation: 'horizontal', // menu alignment (horizontal,vertical)
+ menuOffsetLeft: 5, // left offset of primary menu
+ menuOffsetTop: 5, // top offset of primary menu
+ bg: null, // set bg on init
+ image: null, // set image on init
+ imageStretch: false, // stretch smaller images to full canvans dimensions
+ onShapeDown: null, // callback for draw down event
+ onShapeMove: null, // callback for draw move event
+ onShapeUp: null // callback for draw up event
+ };
+})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/src/wPaint.utils.js b/static/js/wpaint/src/wPaint.utils.js
new file mode 100644
index 0000000..7f1346a
--- /dev/null
+++ b/static/js/wpaint/src/wPaint.utils.js
@@ -0,0 +1,70 @@
+(function () {
+ if (!String.prototype.capitalize) {
+ String.prototype.capitalize = function () {
+ return this.slice(0, 1).toUpperCase() + this.slice(1);
+ };
+ }
+})();
+
+(function ($) {
+ $.fn.realWidth = function (type, margin, $el) {
+ var width = null, $div = null, method = null;
+
+ type = type === 'inner' || type === 'outer' ? type : '';
+ method = type === '' ? 'width' : type + 'Width';
+ margin = margin === true ? true : false;
+ $div = $(this).clone().css({position: 'absolute', left: -10000}).appendTo($el || 'body');
+ width = margin ? $div[method](margin) : $div[method]();
+
+ $div.remove();
+
+ return width;
+ };
+
+ $.fn.realHeight = function (type, margin, $el) {
+ var height = null, $div = null, method = null;
+
+ type = type === 'inner' || type === 'outer' ? type : '';
+ method = type === '' ? 'height' : type + 'Height';
+ margin = margin === true ? true : false;
+ $div = $(this).clone().css({position: 'absolute', left: -10000}).appendTo($el || 'body');
+ height = margin ? $div[method](margin) : $div[method]();
+
+ $div.remove();
+
+ return height;
+ };
+
+ $.fn.bindMobileEvents = function () {
+ $(this).on('touchstart touchmove touchend touchcancel', function () {
+ var touches = (event.changedTouches || event.originalEvent.targetTouches),
+ first = touches[0],
+ type = '';
+
+ switch (event.type) {
+ case 'touchstart':
+ type = 'mousedown';
+ break;
+ case 'touchmove':
+ type = 'mousemove';
+ event.preventDefault();
+ break;
+ case 'touchend':
+ type = 'mouseup';
+ break;
+ default:
+ return;
+ }
+
+ var simulatedEvent = document.createEvent('MouseEvent');
+
+ simulatedEvent.initMouseEvent(
+ type, true, true, window, 1,
+ first.screenX, first.screenY, first.clientX, first.clientY,
+ false, false, false, false, 0/*left*/, null
+ );
+
+ first.target.dispatchEvent(simulatedEvent);
+ });
+ };
+})(jQuery); \ No newline at end of file
diff --git a/static/js/wpaint/test/dev.html b/static/js/wpaint/test/dev.html
new file mode 100644
index 0000000..b9cd1bf
--- /dev/null
+++ b/static/js/wpaint/test/dev.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width" />
+
+ <title>Websanova :: wPaint</title>
+
+ <link rel="icon" type="image/vnd.microsoft.icon" href="./demo/img/favicon.ico" />
+ <script type="text/javascript" src="../lib/jquery.1.10.2.min.js"></script>
+</head>
+<body>
+
+ <!-- jQuery UI -->
+ <script type="text/javascript" src="../lib/jquery.ui.core.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.widget.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.mouse.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.draggable.1.10.3.min.js"></script>
+
+ <!-- wColorPicker -->
+ <link rel="Stylesheet" type="text/css" href="../lib/wColorPicker.min.css" />
+ <script type="text/javascript" src="../lib/wColorPicker.min.js"></script>
+
+ <!-- wPaint -->
+ <link rel="Stylesheet" type="text/css" href="../src/wPaint.css" />
+ <script type="text/javascript" src="../src/wPaint.utils.js"></script>
+ <script type="text/javascript" src="../src/wPaint.js"></script>
+
+ <!-- wPaint main -->
+ <script type="text/javascript" src="../plugins/main/src/fillArea.min.js"></script>
+ <script type="text/javascript" src="../plugins/main/src/wPaint.menu.main.js"></script>
+
+ <!-- wPaint text -->
+ <script type="text/javascript" src="../plugins/text/src/wPaint.menu.text.js"></script>
+
+ <!-- wPaint shapes -->
+ <script type="text/javascript" src="../plugins/shapes/src/shapes.min.js"></script>
+ <script type="text/javascript" src="../plugins/shapes/src/wPaint.menu.main.shapes.js"></script>
+
+ <!-- wPaint file -->
+ <script type="text/javascript" src="../plugins/file/src/wPaint.menu.main.file.js"></script>
+
+ <div id="wPaint-demo1" style="position:relative; width:500px; height:200px; background-color:#7a7a7a; margin:70px auto 20px auto;"></div>
+
+ <center style="margin-bottom: 50px;">
+ <input type="button" value="toggle menu" onclick="console.log($('#wPaint-demo1').wPaint('menuOrientation')); $('#wPaint-demo1').wPaint('menuOrientation', $('#wPaint-demo1').wPaint('menuOrientation') === 'vertical' ? 'horizontal' : 'vertical');"/>
+ </center>
+
+ <center id="wPaint-img"></center>
+
+ <script type="text/javascript">
+ var images = [
+ '/test/uploads/wPaint.png',
+ ];
+
+ function saveImg(image) {
+ var _this = this;
+
+ $.ajax({
+ type: 'POST',
+ url: '/test/upload.php',
+ data: {image: image},
+ success: function (resp) {
+
+ // internal function for displaying status messages in the canvas
+ _this._displayStatus('Image saved successfully');
+
+ // doesn't have to be json, can be anything
+ // returned from server after upload as long
+ // as it contains the path to the image url
+ // or a base64 encoded png, either will work
+ resp = $.parseJSON(resp);
+
+ // update images array / object or whatever
+ // is being used to keep track of the images
+ // can store path or base64 here (but path is better since it's much smaller)
+ images.push(resp.img);
+
+ // do something with the image
+ $('#wPaint-img').append($('<img/>').attr('src', image));
+ }
+ });
+ }
+
+ function loadImgBg () {
+
+ // internal function for displaying background images modal
+ // where images is an array of images (base64 or url path)
+ // NOTE: that if you can't see the bg image changing it's probably
+ // becasue the foregroud image is not transparent.
+ this._showFileModal('bg', images);
+ }
+
+ function loadImgFg () {
+
+ // internal function for displaying foreground images modal
+ // where images is an array of images (base64 or url path)
+ this._showFileModal('fg', images);
+ }
+
+ function createCallback(cbName) {
+ return function() {
+ if (console) {
+ console.log(cbName, arguments);
+ }
+ }
+ }
+
+ // init wPaint
+ $('#wPaint-demo1').wPaint({
+ menuOffsetLeft: -35,
+ menuOffsetTop: -50,
+ saveImg: saveImg,
+ loadImgBg: loadImgBg,
+ loadImgFg: loadImgFg,
+ onShapeDown: createCallback('onShapeDown'),
+ onShapeUp: createCallback('onShapeUp'),
+ onShapeMove: createCallback('onShapeDMove')
+ });
+ </script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/static/js/wpaint/test/fullscreen.html b/static/js/wpaint/test/fullscreen.html
new file mode 100644
index 0000000..91b3e15
--- /dev/null
+++ b/static/js/wpaint/test/fullscreen.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width" />
+
+ <title>Websanova :: Paint</title>
+
+ <script type="text/javascript" src="../lib/jquery.1.10.2.min.js"></script>
+</head>
+<body>
+ <!-- jQuery UI -->
+ <script type="text/javascript" src="../lib/jquery.ui.core.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.widget.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.mouse.1.10.3.min.js"></script>
+ <script type="text/javascript" src="../lib/jquery.ui.draggable.1.10.3.min.js"></script>
+
+ <!-- wColorPicker -->
+ <link rel="Stylesheet" type="text/css" href="../lib/wColorPicker.min.css" />
+ <script type="text/javascript" src="../lib/wColorPicker.min.js"></script>
+
+ <!-- wPaint -->
+ <link rel="Stylesheet" type="text/css" href="../src/wPaint.css" />
+ <script type="text/javascript" src="../src/wPaint.utils.js"></script>
+ <script type="text/javascript" src="../src/wPaint.js"></script>
+
+ <!-- wPaint main -->
+ <script type="text/javascript" src="../plugins/main/src/fillArea.min.js"></script>
+ <script type="text/javascript" src="../plugins/main/src/wPaint.menu.main.js"></script>
+
+ <!-- wPaint text -->
+ <script type="text/javascript" src="../plugins/text/src/wPaint.menu.text.js"></script>
+
+ <!-- wPaint shapes -->
+ <script type="text/javascript" src="../plugins/shapes/src/shapes.min.js"></script>
+ <script type="text/javascript" src="../plugins/shapes/src/wPaint.menu.main.shapes.js"></script>
+
+ <!-- wPaint file -->
+ <script type="text/javascript" src="../plugins/file/src/wPaint.menu.main.file.js"></script>
+
+ <div id="wPaint" style="position:relative; width:200px; height:200px; background:#CACACA;"></div>
+
+ <style>
+ body, html {
+ margin: 0px;
+ overflow: hidden;
+ }
+ </style>
+
+ <script type="text/javascript">
+
+ // update elements dimensions
+ // call wPaint('resize')
+ $(window).resize(function () {
+ $('#wPaint').css({
+ width: $(window).width(),
+ height: $(window).height()
+ })
+ .wPaint('resize');
+ })
+
+ // init size based on browser dimensions
+ $(window).resize();
+
+ // set test image
+ // get from tapping enter below
+ $('#wPaint').wPaint({
+ image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAYAAAErCAYAAABNUIGWAAARs0lEQVR4nO3dzY4dV7UH8PUkvMh9CXgC9yATBr5vgMSAGSNGSJdRELN7lUQRwys5EoMgpCsEkWywxFWIMCDHOI4SN273x2LgOnT1PlWnz8euj3Pq95OOlNh99l51Plpe/9q1KwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmKnMuMmMm6nrAAAAAEbWhALZPIQDAAAAsCStUCAzIyuPfZkZ1/c8LmvOCQAAAOygdjCQGReZcVWsRLjvIRwAAACAKRwSDGTGeSsE2CUIKB83mXE+1DECAAAAPXYJBjLj6x2DgJt7LiMox1iNawUBAAAAjKFpyK9bTXn56Gvi7wsD7m3umz0IXF7ATppw6mrqOgAAAE5C08QfcgnATdOoXew5/2XPCgR3SGBN81lbfUaEAwAAAIfaIxhYBQHV9wTId3sWtAMCqwa4I91eEwAAoK4tLiX493X/mfF0hHouNX70EQwAAABUVq4ImLqeiLXmz6oB/k0wAAAAUNlMgwGrBugkGAAAAKhsjsFAhFUDdBMMAAAAVDbjYOBKA0hJMAAAAFDZjIOBr4raPp26JqYnGAAAgIVodsm/nrqOJZhrMBDhcgLWCQYAAGABmlDAP/xHMvNgwCaE3CEYAACABSiCgcyMq6lrOmVzDgYi1hpBq0gWTjAAAAALUf7jPzPOp67pVB1BMHBZ1OizsGCCAQAAWIjM+KIjHPhi6rpO0dyDgQjNILd8FgAAYEEy41wT8E5mPMiMBwONfQzBwHlRp40IF8rvBAAAWJjiXvaLvMY8M85ax382wPhH8foWe0/Mtk6GJRgAAIAFWnojkBkfto7/wwHGbwcDX9YevxbBABF+HwAAwGIteWf6zHjUOvZHA4wvGOBoCAYAAGChiqZwUc1AZjxuHfvjAcY/lmDgaqmfAW4JBgAAYMGWumogM563jvv5AOO3g4H/rD1+LZnxZVHr/05dE+MTDAAAwIItddVAZrxsHffLAcaf9R0J2oqm0J0JFkgwAAAAC7fEVQOCgVv2GUAwAAAAC7fEpiAzvm0d87eVx/5IMMAxWeLvAAAAoGWJTUFmnLeO+bzy2F8KBjgmS/wdAAAAtCyxKciMi9YxX1QeWzDAUVni7wAAAKBliRsQZsbb1jG/rTy2YICjIhgAAICFK86eL6IpyIzLoXbib4cCggGOgWAAAAA4ql30a8iMq9YxX1UeWzDAUSm+D4IBAABYouKMYdVr7udIMHBLMEBmvBAMAADAwhXBwMk3h4KBW4IBItZ+B7yYuh4AAGBkS9uAcMhmWDDAMSqCgaphGQAAcAQy4+uiof1i6pqGJBi4JRggwj4DAABArJ0xrLpT/9wIBm4JBoiwzwAAABDLOmMoGLglGGDFPgMAALBwS7qcoGiA3lQeWzDAUbLPAAAAsIjLCTLjs/bKiMx4WHn8tWAgMx5kxoOa89QiGDhdzSqgm+Zx7/d56M9CZvxXZnww1+8CAAAQa8HASV5OMELzk8XjrPXfv86M/6g95yEEA6epuDRoq+90Zryu/f3PjP/OjL9kxnn5vagxPgAAUNlYwUBmXDYN6airEjLjYXGMnw0wx6ZgYPW4yIxHtefexxJWiSxRTzCwWiVz3fO4KR7l31+1HpeZ8bb1uGia//Pm/2965hcMAADAnI0RDBRnqEdtRpuGZejj67qU4FcbmrTHQ9SxZa2XYwRBTCPvXkqwqUkf8/FCKAAAADM2ZDDQnEnsalBGa0hzhM3VuoKB5s8/zIy/zikgyAWsFsh3KzYWf037hOHATb5bTfA8Mz6Z+nUAAADuMUQwkLeXDWxqHkZpSovjezrQHJ3BQOvvv5MZn/SFJJnxVWb8fIjaWjVcDhkCzUXevYzjbOp6urS+H6vHYN+F1vu+7+UE215K8I/M+J+hjgMAABhQVt6IbkMgsGoyRmtMM+PpGPPt0nBnxpOegGAVElxnxj8q1vayZ75TXi0wy2CgI5wpvx9/mqiudk2vp6gBAACYUHPWr0rznOu7kK+a3fPm778q/u7TOkfRW88oQcQ+Z+LvCQhWr9vHB9bVG9IcMu7cNeHA2dR1tOXdvR3uW4Y/9gad7lIBAABLVjkYuLmvwSl+ZtAmpGh4BtlfoJln7yX6mfEsb5dzV7nkYsOZ6dHvCrF0eXcpf9fqkN4VBCPWOOpKHgAAYIbaDcmez+9qRM83/OwoDVBR06uR5tm7scqMn+fduyi0m8i3mfGLDc9dXbfet4/By33rYj89qwTWArMNQc5gn9li/k8P/R0AAAAcuX2bguy/68DGZj/Xl7gPchZ7rGanVjDQGu/jDWeSuzaR2/SzVghMpOd96X0/MuP92p+lPWu9GGteAABgJvZpCjqa+50a0eL51RugzHg1VoM1VDOXGd9saPpndZ06d3UENlu9J8XndrRLCtI+AwAAsGy7BgPZvUR652aimLdaI5sZPyrGHnovg0HP8mbGmy0CgtVKAoHAiFarNjr+rHx/tt7jouP5g18GkhX3GgEAAI7QrmcLO86Edu4nsMU4l7WbkZ6z7IM2y0MHA6153mtCgvJSAmHABPq+Nx3fj503vhzrM1XM2f7ODHrHEAAAYGZ2CQZy/ZaEjw+cu9qqgZ4ztYMvi56iiWN6fe97jc9DZrwcM9xq5mzXPdhdPAAAgBnaJhjI7p3Ta2y0V+Xa5ly/vOEmM745tL4t515UMJC3189v85jN9eqrmiqPV17KUX5HDvlMD7oPxz3zzeZ9AwAARpD33Me850x8lbOYtZqfcpzM+NGhte0w92KCgY7Gd5vH5E3mEO9RRzCwtu/DgeP/rhjvJzXq3jDfaBt2AgAAM5MZXxcNyBetvzvo7gNbzF2e6d+riSwas9c1attz7pNtqPYMBeYSDNypqcJ4ZRC1tnqgUt2DbNDZM9efl/A5BgAAenQ1IF1Ne2Y8GWDuMnzYuQGq2fTtMffJLsHOu5cNdAVEmx6rn/m/GRzHWv0Vx1sLBirWXX2Dznvmm+x7BAAATKyruS2anTFv+bfTXJnxesozncVr92rs+YfSEdhsdUa8I1CquspkX101HTDWKMFAx1xj3mHjz0POBQAAzEx5ZjLXLy/448Dz733Wfeoz9sXr9Pux5x9K3yqBLZ7XFyisPltXmfFojGPoqK3KqoGOYGCwSyfGXDVQHMvJhFwAAMCWNjQ7Yyxh3qv5yfXbJ45+VvoUg4GOZnfr6+Zz/fr7TSHBqO9XZrxf1LDXfhQbjmuQ78pYqwamDtkAAICJbTjTO0rzljteTpAZPxk7wOipo/1a/XiKGmrq+Bzs9Lp2BEw/zu7bXbZ/5ulQx9NR38GrBjYcy3ntepv5Rlk1IBgAAICF6wkGRmu2c8c7FHQ0oIPezm1DHQcvTZ+TQ8OWTc+/JyC4zoyH9Y6kt77Xxbzv7zHGaKsFWnMOvmpAMAAAAAvX0ZiPvjS/I5xYa04y46cdzeUgZ2q3ceLBwM7N4TbBQmb8oC+Iav78s8OPZGONh6yIuJoiQBtj1YBgAAAAWDUf181jkl3kN4UDmfHPjlBgsgYmM753SsFA8drv1XzusuIgM55uWEFwkxlv9juSnWrc6X3rCzSGqLNj7kFXDQgGAACA2egKB3oaskmbl8z4/YkFAwfv2bDPGJnxdkNAUP09zoxvDggG9rpbQw1DrxoQDAAAALPSEwS0zyb/cwY1CgY2j3G143M/a973wVeFFON/s8PzJgsGOuavfWtEwQAAADAvfcu2M+OnU9cWEZEZf5miORzKocFAjWChGedhx3u/U8iwxRx7NfYzCAbKvUA+rzi2YAAAAJifOTcreXcjuqqN6xQOaewz42+1z2Z3hAOPDh2zNfZe+wxMHQx01FBt7jl/1wAAgIVrzpJOshniJsXZ29nVt6uORnzfM+k1m9Whxt1rn4GZBAOfF/NX+exlxt9PaQUMAADA4DLjTauRGmQH/bF1NL73njnOjIviORcV63k0RBPcjL3zPgM9wcDoZ9ezwh0kOsb8QDAAAACwg1MMBiLWz9I3j97md6iz+q3xr4pG/LeVxt35rH9PMDD6ZST57laPQ6ykOJnNNAEAAAZ3qsFARP+S+TIo6AgRfjlCPTeZ8d0KY+aujXDX63JoHfsqank6wJh/rzEmAADAycqM81YTdT51PTXl5ttFthv0UZbUZ8Z3a69MqBQMTLbkvqilyqqFYsyTCrsAAACqy4xXrSbq1dT11NaEA+1VAhtDghHq+W0x58sDxzv2YOCqdh3pzgQAAADbO/VgoNQKCiYJBpoa3taa8wSCgQ+KWh5WGPPNHI4NAADgKGTGi1YT9WLqesZSrCQY/exyMe9OdynIjLPMeND891EHAx31HLz0P92yEAAAYHt5937yn09dzxSakGDUJeeZ8axozp9s+byz1nPOTiQYqH6Gf9fXBAAAYLEy4w+tJuoPU9ezJMX19Vtt/JgZP2w954enEAx01HTwJpjFeB/UqBEAAOAkZcYnrQbqk6nrWZLitc/M+NkWz/mo9fMfnVAwcF6znuIY3bIQAACgT9loTl3P0hR7PHy4xc/fCXJOJRiIWF81kBkPstlLYY+x3JkAAABgG+U161PXszS7vv4nHgzcuXXhIZ/LIfYtAAAAOFlNc3o2dR1LtcvrX+4JsWcwcF08bxZn1DPjsjyeA4KB94pw4TcDlAwAAADjyuIuEvsEA804q9s1ziIUiIjIjLcdocCvDhjP5QQAAACclmJPguf7BgNzlBkXxfFcHTjeb4pLE96rVSsAAABMIjP+2j4LfmLBwLPieJ5XGLN9OcGbGnUCAADAZDLj+z3X4R99MBCxts/AkwrjXRWv0VmFMgEAAGA6pxwMRERkxpMaoUAzVhkM3HtLSAAAAJi1zPhbTzDgtnyFjmDg8dQ1AQAAwMEy401HMGDn/ULH6/T/U9cEAAAAVWTGL4qm99XUNc1NZnwsGAAAAOCkZcYroUC/4s4EXicAAABYkiIYsA8DAAAALElmvCwuJ3g2dU0AAADAiDLjWjAAAAAAC5YZz4QCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA4V/ilvYw30ZUiAAAAABJRU5ErkJggg=='
+ });
+
+ // get an image for testing (just tap enter key)
+ $(document).keypress(function (e) {
+ if (e.keyCode === 13) {
+ console.log($('#wPaint').wPaint('image'));
+ }
+ });
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/static/js/wpaint/test/upload.php b/static/js/wpaint/test/upload.php
new file mode 100644
index 0000000..f0187b5
--- /dev/null
+++ b/static/js/wpaint/test/upload.php
@@ -0,0 +1,11 @@
+<?php
+
+$image = imagecreatefrompng($_POST['image']);
+$id = uniqid();
+
+imagealphablending($image, false);
+imagesavealpha($image, true);
+imagepng($image, 'uploads/wPaint-' . $id . '.png');
+
+// return image path
+echo '{"img": "/test/uploads/wPaint-' . $id . '.png"}';
diff --git a/static/js/wpaint/test/uploads/test1.png b/static/js/wpaint/test/uploads/test1.png
new file mode 100644
index 0000000..a0c6511
--- /dev/null
+++ b/static/js/wpaint/test/uploads/test1.png
Binary files differ
diff --git a/static/js/wpaint/test/uploads/test2.png b/static/js/wpaint/test/uploads/test2.png
new file mode 100644
index 0000000..68c25e0
--- /dev/null
+++ b/static/js/wpaint/test/uploads/test2.png
Binary files differ
diff --git a/static/js/wpaint/test/uploads/test3.png b/static/js/wpaint/test/uploads/test3.png
new file mode 100644
index 0000000..c92d3cc
--- /dev/null
+++ b/static/js/wpaint/test/uploads/test3.png
Binary files differ
diff --git a/static/js/wpaint/test/uploads/wPaint.png b/static/js/wpaint/test/uploads/wPaint.png
new file mode 100644
index 0000000..c19ab7f
--- /dev/null
+++ b/static/js/wpaint/test/uploads/wPaint.png
Binary files differ
diff --git a/static/js/wpaint/wPaint.jquery.json b/static/js/wpaint/wPaint.jquery.json
new file mode 100644
index 0000000..ab42ec5
--- /dev/null
+++ b/static/js/wpaint/wPaint.jquery.json
@@ -0,0 +1,38 @@
+{
+ "name": "wPaint",
+ "title": "wPaint jQuery Paint Plugin",
+ "description": "A jQuery paint plugin for a simple drawing surface that you can easily pop into your pages, similar to the basic windows paint program.",
+ "keywords": [
+ "websanova",
+ "wPaint",
+ "paint",
+ "canvas",
+ "html5"
+ ],
+ "version": "2.5.0",
+ "author": {
+ "name": "Websanova",
+ "email": "rob@websanova.com",
+ "url": "http://websanova.com"
+ },
+ "maintainers": [
+ {
+ "name": "Websanova",
+ "email": "rob@websanova.com",
+ "url": "http://websanova.com"
+ }
+ ],
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/websanova/wPaint#license"
+ }
+ ],
+ "bugs": "https://github.com/websanova/wPaint/issues",
+ "homepage": "http://wpaint.websanova.com",
+ "docs": "https://github.com/websanova/wPaint#wpaintjs",
+ "download": "https://github.com/websanova/wPaint/tags",
+ "dependencies": {
+ "jquery": ">=1.5"
+ }
+}
diff --git a/static/js/wpaint/wPaint.min.css b/static/js/wpaint/wPaint.min.css
new file mode 100644
index 0000000..29de08b
--- /dev/null
+++ b/static/js/wpaint/wPaint.min.css
@@ -0,0 +1,66 @@
+.wPaint-menu{position:absolute !important;display:inline-block;*display:inline;zoom:1;line-height:0;z-index:99}
+.wPaint-menu-behind{z-index:98}
+.wPaint-menu-holder{position:relative;margin:0 1px 1px 0}
+.wPaint-menu-handle{display:inline-block;*display:inline;zoom:1}
+.wPaint-menu-icon{position:relative;vertical-align:top}
+.wPaint-menu-icon-img{position:relative;display:inline-block;*display:inline;zoom:1;background-repeat:no-repeat;overflow:hidden}
+.wPaint-menu-select-holder{position:absolute;left:1px;z-index:10;overflow:hidden}
+.wPaint-menu-select{position:relative;text-align:center;overflow-y:scroll;z-index:100}
+.wPaint-menu-select-option.first{border-top:0}
+.wPaint-menu-alignment-horizontal .wPaint-menu-icon{display:inline-block;*display:inline;zoom:1}
+.wPaint-menu-alignment-vertical .wPaint-menu-icon{display:block}
+.wPaint-status{position:absolute;display:none;right:0;bottom:0}
+.wPaint-modal-bg{position:absolute;left:0;top:0;width:100%;height:100%}
+.wPaint-modal{position:absolute;display:inline-block;*display:inline;zoom:1}
+.wPaint-modal-holder{display:inline-block;*display:inline;zoom:1;overflow:hidden}
+.wPaint-modal-content{overflow-y:scroll;width:100%;height:100%}
+.wPaint-modal-close{position:absolute}
+.wPaint-text-input{margin:0;padding:0;outline-width:0;word-wrap:break-word;overflow:hidden}
+.wPaint-modal-img-holder{line-height:0}
+.wPaint-modal-img{display:inline-block;*display:inline;zoom:1}
+.wPaint-menu-holder{border-style:solid;border-width:1px;-webkit-box-shadow:3px 3px 5px #555;box-shadow:3px 3px 5px #555}
+.wPaint-menu-handle{cursor:pointer}
+.wPaint-menu-icon{border-style:solid;border-width:1px;cursor:pointer}
+.wPaint-menu-icon.disabled{cursor:default}
+.wPaint-menu-icon.disabled .wPaint-menu-icon-img{opacity:.3;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";filter:alpha(opacity=30)}
+.wPaint-menu-icon-img{font-family:verdana;font-weight:bold;text-align:center}
+.wPaint-menu-select-holder{border-style:solid;border-width:1px;-webkit-box-shadow:1px 1px 2px #666;box-shadow:1px 1px 2px #666}
+.wPaint-menu-select{font-family:verdana;text-align:center}
+.wPaint-menu-select-option{border-top-style:solid;border-top-width:1px;cursor:pointer}
+.wPaint-menu-icon-select-img{background-repeat:no-repeat}
+.wPaint-menu-icon-group-arrow{position:absolute;right:1px;bottom:1px}
+.wPaint-menu-alignment-horizontal .wPaint-menu-handle{border-right-style:solid;border-right-width:1px}
+.wPaint-menu-alignment-vertical .wPaint-menu-handle{border-bottom-style:solid;border-bottom-width:1px}
+.wPaint-status{font-size:10px;font-family:verdana;line-height:10px;height:10px;background-color:#3a3a3a;color:#f0f0f0;padding:5px;opacity:.5;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";filter:alpha(opacity=50)}
+.wPaint-modal-bg{background-color:#3a3a3a;opacity:.8;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";filter:alpha(opacity=80)}
+.wPaint-modal-holder{height:100px;-webkit-box-shadow:3px 3px 5px #555;box-shadow:3px 3px 5px #555;-webkit-border-radius:5px;border-radius:5px;border-style:solid;border-width:2px;cursor:default}
+.wPaint-modal-close{right:-7px;top:-7px;-webkit-border-radius:10px;border-radius:10px;font-size:8px;line-height:14px;padding:0 4px;font-weight:bold;border-style:solid;border-width:2px;cursor:pointer}
+.wPaint-text-input{border:dotted #00f 1px;background:none}
+.wPaint-modal-img-holder{border:solid #333 1px;-webkit-border-radius:5px;border-radius:5px;margin:3px;padding:2px;cursor:pointer}
+.wPaint-modal-img{width:100px;-webkit-border-radius:4px;border-radius:4px;margin-bottom:0}
+.wPaint-theme-standard .wPaint-menu-holder{-webkit-border-radius:7px;border-radius:7px}
+.wPaint-theme-standard .wPaint-menu-select-holder{-webkit-border-radius:5px;border-radius:5px}
+.wPaint-theme-standard .wPaint-menu-icon{-webkit-border-radius:7px;border-radius:7px}
+.wPaint-theme-standard .wPaint-menu-icon-img{margin:6px 5px 5px 6px;width:18px;height:18px;line-height:18px;font-size:12px}
+.wPaint-theme-standard .wPaint-menu-colorpicker .wPaint-menu-icon-img{margin:3px 2px 2px 3px;width:24px;height:24px;-webkit-border-radius:5px;border-radius:5px}
+.wPaint-theme-standard .wPaint-menu-icon-group .wPaint-menu-select-option{padding:4px}
+.wPaint-theme-standard .wPaint-menu-icon-group-arrow{width:5px;height:3px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAADCAYAAABbNsX4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK6wAACusBgosNWgAAABZ0RVh0Q3JlYXRpb24gVGltZQAwOC8xMS8xMyj8hykAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAKElEQVQImV3IwQ0AMAyDQKerspZ3pa9IVXmdGMB8nbbzjrYTNWoA1xeQ3RPyxUyE/gAAAABJRU5ErkJggg==")}
+.wPaint-theme-standard .wPaint-menu-select{line-height:10px;font-size:10px;max-height:136px}
+.wPaint-theme-standard .wPaint-menu-select-option{max-width:50px;padding:4px 7px}
+.wPaint-theme-standard .wPaint-menu-icon-select-img{width:18px;height:18px}
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal.wPaint-menu-nohandle .wPaint-menu-holder{padding-left:4px}
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal .wPaint-menu-icon{margin:4px 5px 4px 0}
+.wPaint-theme-standard .wPaint-menu-alignment-horizontal .wPaint-menu-handle{width:30px;height:39px;margin-right:5px;border-top-left-radius:7px;border-bottom-left-radius:7px}
+.wPaint-theme-standard .wPaint-menu-alignment-vertical.wPaint-menu-nohandle .wPaint-menu-holder{padding-top:4px}
+.wPaint-theme-standard .wPaint-menu-alignment-vertical .wPaint-menu-icon{margin:0 4px 5px 4px}
+.wPaint-theme-standard .wPaint-menu-alignment-vertical .wPaint-menu-handle{width:39px;height:30px;margin-bottom:5px;border-top-left-radius:7px;border-top-right-radius:7px}
+.wPaint-theme-classic .wPaint-menu-holder{border-color:#dadada;background-color:#f0f0f0}
+.wPaint-theme-classic .wPaint-menu-handle{background-color:#dadada;-webkit-box-shadow:inset 1px 1px 3px #fff;box-shadow:inset 1px 1px 3px #fff;border-color:#dadada}
+.wPaint-theme-classic .wPaint-menu-icon{border-color:#b9b9b9;background-color:#b9b9b9;-webkit-box-shadow:inset 2px 2px 3px #eee,1px 1px 2px #666;box-shadow:inset 2px 2px 3px #eee,1px 1px 2px #666}
+.wPaint-theme-classic .wPaint-menu-icon.hover,.wPaint-theme-classic .wPaint-menu-icon.active{border-color:#9cf;background-color:#acf}
+.wPaint-theme-classic .wPaint-menu-icon-img{color:#696969}
+.wPaint-theme-classic .wPaint-menu-select-holder{border-color:#cacaca}
+.wPaint-theme-classic .wPaint-menu-select{color:#494949}
+.wPaint-theme-classic .wPaint-menu-select-option{-webkit-box-shadow:inset 2px 2px 3px #fff;box-shadow:inset 2px 2px 3px #fff;border-top-color:#cacaca;background-color:#f0f0f0}
+.wPaint-theme-classic .wPaint-menu-select-option:hover{-webkit-box-shadow:inset 1px 1px 1px #fff;box-shadow:inset 1px 1px 1px #fff;background-color:#9cf;color:#f0f0f0}
+.wPaint-theme-classic .wPaint-modal-close,.wPaint-theme-classic .wPaint-modal-holder{border-color:#3a3a3a;background-color:#f0f0f0}
diff --git a/static/js/wpaint/wPaint.min.js b/static/js/wpaint/wPaint.min.js
new file mode 100644
index 0000000..3ea04e6
--- /dev/null
+++ b/static/js/wpaint/wPaint.min.js
@@ -0,0 +1 @@
+/*! wPaint - v2.5.0 - 2014-03-01 */!function(a){"use strict";function b(b,c){this.$el=a(b),this.options=c,this.init=!1,this.menus={primary:null,active:null,all:{}},this.previousMode=null,this.width=this.$el.width(),this.height=this.$el.height(),this.ctxBgResize=!1,this.ctxResize=!1,this.generate(),this._init()}function c(a,b,c){this.wPaint=a,this.options=c,this.name=b,this.type=a.menus.primary?"secondary":"primary",this.docked=!0,this.dockOffset={left:0,top:0},this.generate()}b.prototype={generate:function(){function b(b){var c=b?b.capitalize():"",d="canvas"+c,e="ctx"+c;return f[d]=document.createElement("canvas"),f[e]=f[d].getContext("2d"),f["$"+d]=a(f[d]),f["$"+d].attr("class","wPaint-canvas"+(b?"-"+b:"")).attr("width",f.width+"px").attr("height",f.height+"px").css({position:"absolute",left:0,top:0}),f.$el.append(f["$"+d]),f["$"+d]}function c(a){a.preventDefault(),a.stopPropagation(),f.draw=!0,a.canvasEvent="down",f._closeSelectBoxes(),f._callShapeFunc.apply(f,[a])}function d(a){f.draw&&(a.canvasEvent="move",f._callShapeFunc.apply(f,[a]))}function e(a){f.draw&&(f.draw=!1,a.canvasEvent="up",f._callShapeFunc.apply(f,[a]))}if(this.init)return this;var f=this;b("bg"),b("").on("mousedown",c).bindMobileEvents(),b("temp").hide(),a(document).on("mousemove",d).on("mousedown",a.proxy(this._closeSelectBoxes,this)).on("mouseup",e),this.setTheme(this.options.theme)},_init:function(){var a=null,b=null;this.init=!0;for(a in this.options)b="set"+a.capitalize(),this[b]&&this[b](this.options[a]);this._fixMenus(),this.menus.primary._getIcon(this.options.mode).trigger("click")},resize:function(){var a=this.getBg(),b=this.getImage();this.width=this.$el.width(),this.height=this.$el.height(),this.canvasBg.width=this.width,this.canvasBg.height=this.height,this.canvas.width=this.width,this.canvas.height=this.height,this.ctxBgResize===!1&&(this.ctxBgResize=!0,this.setBg(a,!0)),this.ctxResize===!1&&(this.ctxResize=!0,this.setImage(b,"",!0,!0))},setTheme:function(a){var b,c;for(a=a.split(" "),this.$el.attr("class",(this.$el.attr("class")||"").replace(/wPaint-theme-.+\s|wPaint-theme-.+$/,"")),b=0,c=a.length;c>b;b++)this.$el.addClass("wPaint-theme-"+a[b])},setMode:function(a){this.setCursor(a),this.previousMode=this.options.mode,this.options.mode=a},setImage:function(b,c,d,e){function f(){var a=1,b=0,f=0,j=0,k=0,l=h.width,m=h.height;d||((h.width>g.width||h.height>g.height||g.options.imageStretch)&&(b=g.width/h.width,f=g.height/h.height,a=f>b?b:f,l=h.width*a,m=h.height*a),j=(g.width-l)/2,k=(g.height-m)/2),i.clearRect(0,0,g.width,g.height),i.drawImage(h,j,k,l,m),g[c+"Resize"]=!1,e||g._addUndo()}if(!b)return!0;var g=this,h=null,i="";c="ctx"+(c||"").capitalize(),i=this[c],window.rgbHex(b)?(i.clearRect(0,0,this.width,this.height),i.fillStyle=b,i.rect(0,0,this.width,this.height),i.fill()):(h=new Image,h.src=b.toString(),a(h).load(f))},setBg:function(a,b){return a?void this.setImage(a,"bg",b,!0):!0},setCursor:function(b){b=a.fn.wPaint.cursors[b]||a.fn.wPaint.cursors["default"],this.$el.css("cursor",'url("'+this.options.path+b.path+'") '+b.left+" "+b.top+", default")},setMenuOrientation:function(b){a.each(this.menus.all,function(a,c){c.options.aligment=b,c.setAlignment(b)})},getImage:function(b){var c=document.createElement("canvas"),d=c.getContext("2d");return b=b===!1?!1:!0,a(c).css({display:"none",position:"absolute",left:0,top:0}).attr("width",this.width).attr("height",this.height),b&&d.drawImage(this.canvasBg,0,0),d.drawImage(this.canvas,0,0),c.toDataURL()},getBg:function(){return this.canvasBg.toDataURL()},_displayStatus:function(b){var c=this;this.$status||(this.$status=a('<div class="wPaint-status"></div>'),this.$el.append(this.$status)),this.$status.html(b),clearTimeout(this.displayStatusTimer),this.$status.fadeIn(500,function(){c.displayStatusTimer=setTimeout(function(){c.$status.fadeOut(500)},1500)})},_showModal:function(a){function b(){d.remove(),e.remove(),c._createModal(a)}var c=this,d=this.$el.children(".wPaint-modal-bg"),e=this.$el.children(".wPaint-modal");d.length?e.fadeOut(500,b):this._createModal(a)},_createModal:function(b){function c(){f.fadeOut(500,d)}function d(){e.remove(),f.remove()}b=a('<div class="wPaint-modal-content"></div>').append(b.children());var e=a('<div class="wPaint-modal-bg"></div>'),f=a('<div class="wPaint-modal"></div>'),g=a('<div class="wPaint-modal-holder"></div>'),h=a('<div class="wPaint-modal-close">X</div>');h.on("click",c),f.append(g.append(b)).append(h),this.$el.append(e).append(f),f.css({left:this.$el.outerWidth()/2-f.outerWidth(!0)/2,top:this.$el.outerHeight()/2-f.outerHeight(!0)/2}),f.fadeIn(500)},_createMenu:function(a,b){return b=b||{},b.alignment=this.options.menuOrientation,b.handle=this.options.menuHandle,new c(this,a,b)},_fixMenus:function(){function b(b,d){var e=a(d),f=e.clone();f.appendTo(c.$el),f.outerHeight()===f.get(0).scrollHeight&&e.css({overflowY:"auto"}),f.remove()}var c=this,d=null;for(var e in this.menus.all)d=c.menus.all[e].$menu.find(".wPaint-menu-select-holder"),d.length&&d.children().each(b)},_closeSelectBoxes:function(a){var b,c;for(b in this.menus.all)c=this.menus.all[b].$menuHolder.children(".wPaint-menu-icon-select"),a&&(c=c.not(".wPaint-menu-icon-name-"+a.name)),c.children(".wPaint-menu-select-holder").hide()},_callShapeFunc:function(a){var b=this.$canvas.offset(),c=a.canvasEvent.capitalize(),d="_draw"+this.options.mode.capitalize()+c;a.pageX=Math.floor(a.pageX-b.left),a.pageY=Math.floor(a.pageY-b.top),this[d]&&this[d].apply(this,[a]),this.options["draw"+c]&&this.options["_draw"+c].apply(this,[a]),"Down"===c&&this.options.onShapeDown?this.options.onShapeDown.apply(this,[a]):"Move"===c&&this.options.onShapeMove?this.options.onShapeMove.apply(this,[a]):"Up"===c&&this.options.onShapeUp&&this.options.onShapeUp.apply(this,[a])},_stopPropagation:function(a){a.stopPropagation()},_drawShapeDown:function(a){this.$canvasTemp.css({left:a.PageX,top:a.PageY}).attr("width",0).attr("height",0).show(),this.canvasTempLeftOriginal=a.pageX,this.canvasTempTopOriginal=a.pageY},_drawShapeMove:function(b,c){var d=this.canvasTempLeftOriginal,e=this.canvasTempTopOriginal;c=c||2,b.left=b.pageX<d?b.pageX:d,b.top=b.pageY<e?b.pageY:e,b.width=Math.abs(b.pageX-d),b.height=Math.abs(b.pageY-e),b.x=this.options.lineWidth/2*c,b.y=this.options.lineWidth/2*c,b.w=b.width-this.options.lineWidth*c,b.h=b.height-this.options.lineWidth*c,a(this.canvasTemp).css({left:b.left,top:b.top}).attr("width",b.width).attr("height",b.height),this.canvasTempLeftNew=b.left,this.canvasTempTopNew=b.top,c=c||2,this.ctxTemp.fillStyle=this.options.fillStyle,this.ctxTemp.strokeStyle=this.options.strokeStyle,this.ctxTemp.lineWidth=this.options.lineWidth*c},_drawShapeUp:function(){this.ctx.drawImage(this.canvasTemp,this.canvasTempLeftNew,this.canvasTempTopNew),this.$canvasTemp.hide()},_drawDropperDown:function(a){var b={x:a.pageX,y:a.pageY},c=this._getPixel(this.ctx,b),d=null;d="rgba("+[c.r,c.g,c.b,c.a].join(",")+")",this.options[this.dropper]=d,this.menus.active._getIcon(this.dropper).wColorPicker("color",d)},_drawDropperUp:function(){this.setMode(this.previousMode)},_getPixel:function(a,b){var c=a.getImageData(0,0,this.width,this.height),d=c.data,e=4*(b.y*c.width+b.x);return{r:d[e],g:d[e+1],b:d[e+2],a:d[e+3]}}},c.prototype={generate:function(){this.$menu=a('<div class="wPaint-menu"></div>'),this.$menuHolder=a('<div class="wPaint-menu-holder wPaint-menu-name-'+this.name+'"></div>'),this.options.handle?this.$menuHandle=this._createHandle():this.$menu.addClass("wPaint-menu-nohandle"),"primary"===this.type?(this.wPaint.menus.primary=this,this.setOffsetLeft(this.options.offsetLeft),this.setOffsetTop(this.options.offsetTop)):"secondary"===this.type&&this.$menu.hide(),this.$menu.append(this.$menuHolder.append(this.$menuHandle)),this.reset(),this.wPaint.$el.append(this.$menu),this.setAlignment(this.options.alignment)},reset:function(){function b(a){d._appendItem(a)}var c,d=this,e=a.fn.wPaint.menus[this.name];for(c in e.items)this.$menuHolder.children(".wPaint-menu-icon-name-"+c).length||(e.items[c].name=c,e.items[c].img=d.wPaint.options.path+(e.items[c].img||e.img),b(e.items[c]))},_appendItem:function(a){var b=this["_createIcon"+a.icon.capitalize()](a);a.after?this.$menuHolder.children(".wPaint-menu-icon-name-"+a.after).after(b):this.$menuHolder.append(b)},setOffsetLeft:function(a){this.$menu.css({left:a})},setOffsetTop:function(a){this.$menu.css({top:a})},setAlignment:function(a){var b=this.$menu.css("left");this.$menu.attr("class",this.$menu.attr("class").replace(/wPaint-menu-alignment-.+\s|wPaint-menu-alignment-.+$/,"")),this.$menu.addClass("wPaint-menu-alignment-"+a),this.$menu.width("auto").css("left",-1e4),this.$menu.width(this.$menu.width()).css("left",b),"secondary"===this.type&&("horizontal"===this.options.alignment?this.dockOffset.top=this.wPaint.menus.primary.$menu.outerHeight(!0):this.dockOffset.left=this.wPaint.menus.primary.$menu.outerWidth(!0))},_createHandle:function(){function b(){e.docked=!1,e._setDrag()}function c(){a.each(e.$menu.data("ui-draggable").snapElements,function(a,b){var c=e.$menu.offset(),d=e.wPaint.menus.primary.$menu.offset();e.dockOffset.left=c.left-d.left,e.dockOffset.top=c.top-d.top,e.docked=b.snapping}),e._setDrag()}function d(){e._setIndex()}var e=this,f=a('<div class="wPaint-menu-handle"></div>');return this.$menu.draggable({handle:f}),"secondary"===this.type&&(this.$menu.draggable("option","snap",this.wPaint.menus.primary.$menu),this.$menu.draggable("option","start",b),this.$menu.draggable("option","stop",c),this.$menu.draggable("option","drag",d)),f.bindMobileEvents(),f},_createIconBase:function(b){function c(b){var c=a(b.currentTarget);c.siblings(".hover").removeClass("hover"),c.hasClass("disabled")||c.addClass("hover")}function d(b){a(b.currentTarget).removeClass("hover")}function e(){f.wPaint.menus.active=f}var f=this,g=a('<div class="wPaint-menu-icon wPaint-menu-icon-name-'+b.name+'"></div>'),h=a('<div class="wPaint-menu-icon-img"></div>'),i=h.realWidth(null,null,this.wPaint.$el);return g.attr("title",b.title).on("mousedown",a.proxy(this.wPaint._closeSelectBoxes,this.wPaint,b)).on("mouseenter",c).on("mouseleave",d).on("click",e),a.isNumeric(b.index)&&h.css({backgroundImage:"url("+b.img+")",backgroundPosition:-i*b.index+"px 0px"}),g.append(h)},_createIconGroup:function(b){function c(){h.children(".wPaint-menu-select-holder").is(":visible")||b.callback.apply(f.wPaint,[])}function d(){h.addClass("active").siblings(".active").removeClass("active")}function e(){h.attr("title",b.title).off("click.setIcon").on("click.setIcon",c),h.children(".wPaint-menu-icon-img").css(g),b.callback.apply(f.wPaint,[])}var f=this,g={backgroundImage:"url("+b.img+")"},h=this.$menuHolder.children(".wPaint-menu-icon-group-"+b.group),i=h.length,j=null,k=null,l=null,m=0;return i||(h=this._createIconBase(b).addClass("wPaint-menu-icon-group wPaint-menu-icon-group-"+b.group).on("click.setIcon",c).on("mousedown",a.proxy(this._iconClick,this))),m=h.children(".wPaint-menu-icon-img").realWidth(null,null,this.wPaint.$el),g.backgroundPosition=-m*b.index+"px center",j=h.children(".wPaint-menu-select-holder"),j.length||(j=this._createSelectBox(h),j.children().on("click",d)),l=a('<div class="wPaint-menu-icon-select-img"></div>').attr("title",b.title).css(g),k=this._createSelectOption(j,l).addClass("wPaint-menu-icon-name-"+b.name).on("click",e),b.after&&j.children(".wPaint-menu-select").children(".wPaint-menu-icon-name-"+b.after).after(k),i?void 0:h},_createIconGeneric:function(a){return this._createIconActivate(a)},_createIconActivate:function(a){function b(b){"generic"!==a.icon&&c._iconClick(b),a.callback.apply(c.wPaint,[b])}if(a.group)return this._createIconGroup(a);var c=this,d=this._createIconBase(a);return d.on("click",b),d},_isIconDisabled:function(a){return this.$menuHolder.children(".wPaint-menu-icon-name-"+a).hasClass("disabled")},_setIconDisabled:function(a,b){var c=this.$menuHolder.children(".wPaint-menu-icon-name-"+a);b?c.addClass("disabled").removeClass("hover"):c.removeClass("disabled")},_getIcon:function(a){return this.$menuHolder.children(".wPaint-menu-icon-name-"+a)},_iconClick:function(b){var c=a(b.currentTarget),d=this.wPaint.menus.all;for(var e in d)d[e]&&"secondary"===d[e].type&&d[e].$menu.hide();c.siblings(".active").removeClass("active"),c.hasClass("disabled")||c.addClass("active")},_createIconToggle:function(a){function b(){d.toggleClass("active"),a.callback.apply(c.wPaint,[d.hasClass("active")])}var c=this,d=this._createIconBase(a);return d.on("click",b),d},_createIconSelect:function(b){function c(c){h.children(".wPaint-menu-icon-img").html(a(c.currentTarget).html()),b.callback.apply(g.wPaint,[a(c.currentTarget).html()])}var d,e,f,g=this,h=this._createIconBase(b),i=this._createSelectBox(h);for(d=0,e=b.range.length;e>d;d++)f=this._createSelectOption(i,b.range[d]),f.on("click",c),b.useRange&&f.css(b.name,b.range[d]);return h},_createSelectBox:function(b){function c(a){a.stopPropagation(),g.hide()}function d(){i=setTimeout(function(){g.toggle()},200)}function e(){clearTimeout(i)}function f(){g.toggle()}var g=a('<div class="wPaint-menu-select-holder"></div>'),h=a('<div class="wPaint-menu-select"></div>'),i=null;return g.on("mousedown mouseup",this.wPaint._stopPropagation).on("click",c).hide(),g.css("horizontal"===this.options.alignment?{left:0,top:b.children(".wPaint-menu-icon-img").realHeight("outer",!0,this.wPaint.$el)}:{left:b.children(".wPaint-menu-icon-img").realWidth("outer",!0,this.wPaint.$el),top:0}),b.addClass("wPaint-menu-icon-select").append('<div class="wPaint-menu-icon-group-arrow"></div>').append(g.append(h)),b.hasClass("wPaint-menu-icon-group")?b.on("mousedown",d).on("mouseup",e):b.on("click",f),g},_createSelectOption:function(b,c){var d=b.children(".wPaint-menu-select"),e=a('<div class="wPaint-menu-select-option"></div>').append(c);return d.children().length||e.addClass("first"),d.append(e),e},_setSelectValue:function(a,b){this._getIcon(a).children(".wPaint-menu-icon-img").html(b)},_createIconColorPicker:function(a){function b(){"dropper"===e.wPaint.options.mode&&e.wPaint.setMode(e.wPaint.previousMode)}function c(b){a.callback.apply(e.wPaint,[b])}function d(){f.trigger("click"),e.wPaint.dropper=a.name,e.wPaint.setMode("dropper")}var e=this,f=this._createIconBase(a);return f.on("click",b).addClass("wPaint-menu-colorpicker").wColorPicker({mode:"click",generateButton:!1,dropperButton:!0,onSelect:c,onDropper:d}),f},_setColorPickerValue:function(a,b){this._getIcon(a).children(".wPaint-menu-icon-img").css("backgroundColor",b)},_createIconMenu:function(a){function b(){c.wPaint.setCursor(a.name);var b=c.wPaint.menus.all[a.name];b.$menu.toggle(),c.handle?b._setDrag():b._setPosition()}var c=this,d=this._createIconActivate(a);return d.on("click",b),d},_setDrag:function(){var b=this.$menu,c=null,d=null;b.is(":visible")&&(this.docked&&(c=d=a.proxy(this._setPosition,this),this._setPosition()),this.wPaint.menus.primary.$menu.draggable("option","drag",c),this.wPaint.menus.primary.$menu.draggable("option","stop",d))},_setPosition:function(){var a=this.wPaint.menus.primary.$menu.position();this.$menu.css({left:a.left+this.dockOffset.left,top:a.top+this.dockOffset.top})},_setIndex:function(){var a=this.wPaint.menus.primary.$menu.offset(),b=this.$menu.offset();b.top<a.top||b.left<a.left?this.$menu.addClass("wPaint-menu-behind"):this.$menu.removeClass("wPaint-menu-behind")}},a.support.canvas=document.createElement("canvas").getContext,a.fn.wPaint=function(c,d){function e(){return a.support.canvas?a.proxy(f,this)():(a(this).html("Browser does not support HTML5 canvas, please upgrade to a more modern browser."),!1)}function f(){var d=a.data(this,"wPaint");return d||(d=new b(this,a.extend(!0,{},c)),a.data(this,"wPaint",d)),d}function g(){var b=a.data(this,"wPaint");b&&(b[c]?b[c].apply(b,[d]):void 0!==d?(b[i]&&b[i].apply(b,[d]),b.options[c]&&(b.options[c]=d)):h.push(b[i]?b[i].apply(b,[d]):b.options[c]?b.options[c]:void 0))}if("string"==typeof c){var h=[],i=(d?"set":"get")+c.charAt(0).toUpperCase()+c.substring(1);return this.each(g),h.length?1===h.length?h[0]:h:this}return c=a.extend({},a.fn.wPaint.defaults,c),c.lineWidth=parseInt(c.lineWidth,10),c.fontSize=parseInt(c.fontSize,10),this.each(e)},a.fn.wPaint.extend=function(a,d){function e(c){if(d[c]){var e=b.prototype[c],f=a[c];d[c]=function(){e.apply(this,arguments),f.apply(this,arguments)}}else d[c]=a[c]}var f;d="menu"===d?c.prototype:b.prototype;for(f in a)e(f)},a.fn.wPaint.menus={},a.fn.wPaint.cursors={},a.fn.wPaint.defaults={path:"/",theme:"standard classic",autoScaleImage:!0,autoCenterImage:!0,menuHandle:!0,menuOrientation:"horizontal",menuOffsetLeft:5,menuOffsetTop:5,bg:null,image:null,imageStretch:!1,onShapeDown:null,onShapeMove:null,onShapeUp:null}}(jQuery),function(){String.prototype.capitalize||(String.prototype.capitalize=function(){return this.slice(0,1).toUpperCase()+this.slice(1)})}(),function(a){a.fn.realWidth=function(b,c,d){var e=null,f=null,g=null;return b="inner"===b||"outer"===b?b:"",g=""===b?"width":b+"Width",c=c===!0?!0:!1,f=a(this).clone().css({position:"absolute",left:-1e4}).appendTo(d||"body"),e=c?f[g](c):f[g](),f.remove(),e},a.fn.realHeight=function(b,c,d){var e=null,f=null,g=null;return b="inner"===b||"outer"===b?b:"",g=""===b?"height":b+"Height",c=c===!0?!0:!1,f=a(this).clone().css({position:"absolute",left:-1e4}).appendTo(d||"body"),e=c?f[g](c):f[g](),f.remove(),e},a.fn.bindMobileEvents=function(){a(this).on("touchstart touchmove touchend touchcancel",function(){var a=event.changedTouches||event.originalEvent.targetTouches,b=a[0],c="";switch(event.type){case"touchstart":c="mousedown";break;case"touchmove":c="mousemove",event.preventDefault();break;case"touchend":c="mouseup";break;default:return}var d=document.createEvent("MouseEvent");d.initMouseEvent(c,!0,!0,window,1,b.screenX,b.screenY,b.clientX,b.clientY,!1,!1,!1,!1,0,null),b.target.dispatchEvent(d)})}}(jQuery); \ No newline at end of file
diff --git a/static/meta/bbs.png b/static/meta/bbs.png
new file mode 100644
index 0000000..2ce11b3
--- /dev/null
+++ b/static/meta/bbs.png
Binary files differ
diff --git a/static/meta/faq_1.png b/static/meta/faq_1.png
new file mode 100644
index 0000000..0932873
--- /dev/null
+++ b/static/meta/faq_1.png
Binary files differ
diff --git a/static/meta/faq_2.png b/static/meta/faq_2.png
new file mode 100644
index 0000000..0e101be
--- /dev/null
+++ b/static/meta/faq_2.png
Binary files differ
diff --git a/static/meta/faq_3.png b/static/meta/faq_3.png
new file mode 100644
index 0000000..dd4e657
--- /dev/null
+++ b/static/meta/faq_3.png
Binary files differ
diff --git a/static/meta/ib.png b/static/meta/ib.png
new file mode 100644
index 0000000..7f52460
--- /dev/null
+++ b/static/meta/ib.png
Binary files differ
diff --git a/static/meta/portada_6.jpg b/static/meta/portada_6.jpg
new file mode 100644
index 0000000..3aaff38
--- /dev/null
+++ b/static/meta/portada_6.jpg
Binary files differ
diff --git a/static/meta/portada_7.jpg b/static/meta/portada_7.jpg
new file mode 100644
index 0000000..3f9ed25
--- /dev/null
+++ b/static/meta/portada_7.jpg
Binary files differ
diff --git a/static/meta/portada_8.jpg b/static/meta/portada_8.jpg
new file mode 100644
index 0000000..c28ef87
--- /dev/null
+++ b/static/meta/portada_8.jpg
Binary files differ
diff --git a/static/meta/portada_asp.gif b/static/meta/portada_asp.gif
new file mode 100644
index 0000000..94c1e83
--- /dev/null
+++ b/static/meta/portada_asp.gif
Binary files differ
diff --git a/static/meta/portada_cap1.jpg b/static/meta/portada_cap1.jpg
new file mode 100644
index 0000000..3a980cb
--- /dev/null
+++ b/static/meta/portada_cap1.jpg
Binary files differ
diff --git a/static/meta/portada_orig5.jpg b/static/meta/portada_orig5.jpg
new file mode 100644
index 0000000..5c4e98b
--- /dev/null
+++ b/static/meta/portada_orig5.jpg
Binary files differ
diff --git a/static/meta/portada_toesca.jpg b/static/meta/portada_toesca.jpg
new file mode 100644
index 0000000..0d1c36a
--- /dev/null
+++ b/static/meta/portada_toesca.jpg
Binary files differ
diff --git a/static/meta/portadaphil.jpg b/static/meta/portadaphil.jpg
new file mode 100644
index 0000000..0d9af00
--- /dev/null
+++ b/static/meta/portadaphil.jpg
Binary files differ
diff --git a/static/meta/primeraportada.png b/static/meta/primeraportada.png
new file mode 100644
index 0000000..f4db57b
--- /dev/null
+++ b/static/meta/primeraportada.png
Binary files differ
diff --git a/static/meta/sanvalentin2013.jpg b/static/meta/sanvalentin2013.jpg
new file mode 100644
index 0000000..2596642
--- /dev/null
+++ b/static/meta/sanvalentin2013.jpg
Binary files differ
diff --git a/static/meta/welcome.gif b/static/meta/welcome.gif
new file mode 100644
index 0000000..4eeb392
--- /dev/null
+++ b/static/meta/welcome.gif
Binary files differ
diff --git a/static/meta/welcome.jpg b/static/meta/welcome.jpg
new file mode 100644
index 0000000..ae9f0d2
--- /dev/null
+++ b/static/meta/welcome.jpg
Binary files differ
diff --git a/static/mime/epub.png b/static/mime/epub.png
new file mode 100644
index 0000000..b6800ea
--- /dev/null
+++ b/static/mime/epub.png
Binary files differ
diff --git a/static/mime/epub_small.png b/static/mime/epub_small.png
new file mode 100644
index 0000000..71da41c
--- /dev/null
+++ b/static/mime/epub_small.png
Binary files differ
diff --git a/static/mime/mod.png b/static/mime/mod.png
new file mode 100644
index 0000000..b519172
--- /dev/null
+++ b/static/mime/mod.png
Binary files differ
diff --git a/static/mime/mod_small.png b/static/mime/mod_small.png
new file mode 100644
index 0000000..16887f8
--- /dev/null
+++ b/static/mime/mod_small.png
Binary files differ
diff --git a/static/mime/pdf.png b/static/mime/pdf.png
new file mode 100644
index 0000000..0c0a98a
--- /dev/null
+++ b/static/mime/pdf.png
Binary files differ
diff --git a/static/mime/pdf_small.png b/static/mime/pdf_small.png
new file mode 100644
index 0000000..9af8b84
--- /dev/null
+++ b/static/mime/pdf_small.png
Binary files differ
diff --git a/static/mime/s3m.png b/static/mime/s3m.png
new file mode 100644
index 0000000..ab14689
--- /dev/null
+++ b/static/mime/s3m.png
Binary files differ
diff --git a/static/mime/s3m_small.png b/static/mime/s3m_small.png
new file mode 100644
index 0000000..81da112
--- /dev/null
+++ b/static/mime/s3m_small.png
Binary files differ
diff --git a/static/mime/swf.png b/static/mime/swf.png
new file mode 100644
index 0000000..75c60bb
--- /dev/null
+++ b/static/mime/swf.png
Binary files differ
diff --git a/static/mime/swf_small.png b/static/mime/swf_small.png
new file mode 100644
index 0000000..f6fa215
--- /dev/null
+++ b/static/mime/swf_small.png
Binary files differ
diff --git a/static/mime/torrent.png b/static/mime/torrent.png
new file mode 100644
index 0000000..c580646
--- /dev/null
+++ b/static/mime/torrent.png
Binary files differ
diff --git a/static/mime/torrent_small.png b/static/mime/torrent_small.png
new file mode 100644
index 0000000..a939e12
--- /dev/null
+++ b/static/mime/torrent_small.png
Binary files differ
diff --git a/static/mime/xm.png b/static/mime/xm.png
new file mode 100644
index 0000000..b053172
--- /dev/null
+++ b/static/mime/xm.png
Binary files differ
diff --git a/static/mime/xm_small.png b/static/mime/xm_small.png
new file mode 100644
index 0000000..2510de7
--- /dev/null
+++ b/static/mime/xm_small.png
Binary files differ