From 6348e70daa113e8b3203de8fbc919d08c90d972e Mon Sep 17 00:00:00 2001 From: Mike Macgirvin Date: Thu, 1 Jul 2010 16:48:07 -0700 Subject: [PATCH] Initial checkin --- .gitignore | 2 + .htaccess | 13 + auth.php | 79 + boot.php | 302 + cropper/copper.html | 227 + cropper/cropper.css | 182 + cropper/cropper.js | 566 + cropper/cropper.uncompressed.js | 1331 ++ cropper/lib/builder.js | 101 + cropper/lib/controls.js | 815 + cropper/lib/dragdrop.js | 915 + cropper/lib/effects.js | 958 ++ cropper/lib/prototype.js | 2006 +++ cropper/lib/scriptaculous.js | 47 + cropper/lib/slider.js | 283 + cropper/lib/unittest.js | 383 + cropper/licence.txt | 12 + cropper/marqueeHoriz.gif | Bin 0 -> 1125 bytes cropper/marqueeVert.gif | Bin 0 -> 1141 bytes cropper/tests/castle.jpg | Bin 0 -> 34429 bytes cropper/tests/castleMed.jpg | Bin 0 -> 50584 bytes cropper/tests/example-Basic.htm | 106 + cropper/tests/example-CSS-Absolute.htm | 162 + cropper/tests/example-CSS-Float.htm | 124 + cropper/tests/example-CSS-Relative.htm | 116 + cropper/tests/example-CoordsOnLoad.htm | 108 + .../tests/example-CoordsOnLoadWithRatio.htm | 109 + cropper/tests/example-Dimensions.htm | 225 + cropper/tests/example-DynamicImage.htm | 203 + cropper/tests/example-FixedRatio.htm | 104 + cropper/tests/example-MinimumDimensions.htm | 105 + cropper/tests/example-MinimumWidth.htm | 105 + cropper/tests/example-Preview.htm | 117 + cropper/tests/poppy.jpg | Bin 0 -> 18338 bytes cropper/tests/staticHTMLStructure.htm | 236 + favicon.gif | 0 favicon.ico | 0 images/b_block.gif | Bin 0 -> 83 bytes images/b_drop.gif | Bin 0 -> 138 bytes images/b_drop.png | Bin 0 -> 311 bytes images/b_edit.gif | Bin 0 -> 311 bytes images/b_edit.png | Bin 0 -> 451 bytes images/default-profile-sm.jpg | Bin 0 -> 346 bytes images/default-profile.jpg | Bin 0 -> 490 bytes images/larrw.gif | Bin 0 -> 1004 bytes images/rarrw.gif | Bin 0 -> 999 bytes include/Photo.php | 171 + include/Scrape.php | 80 + include/bbcode.php | 105 + include/datetime.php | 145 + include/dba.php | 138 + include/login.php | 19 + include/security.php | 17 + include/session.php | 76 + include/system_unavailable.php | 6 + index.php | 113 + library/HTML5/Data.php | 120 + library/HTML5/InputStream.php | 284 + library/HTML5/Parser.php | 36 + library/HTML5/Tokenizer.php | 2307 +++ library/HTML5/TreeBuilder.php | 3715 ++++ library/HTML5/named-character-references.ser | 1 + mod/contacts.php | 116 + mod/dfrn_confirm.php | 374 + mod/dfrn_poll.php | 58 + mod/dfrn_request.php | 290 + mod/home.php | 24 + mod/item.php | 68 + mod/login.php | 8 + mod/notifications.php | 98 + mod/photo.php | 25 + mod/profile.php | 136 + mod/profile_photo.php | 227 + mod/profiles.php | 190 + mod/redir.php | 21 + mod/register.php | 175 + mod/settings.php | 170 + mod/test.php | 4 + nav.php | 23 + robots.txt | 0 silho.gif | Bin 0 -> 79 bytes silho.ico | Bin 0 -> 1150 bytes tinymce/changelog.txt | 1075 ++ tinymce/examples/css/content.css | 105 + tinymce/examples/css/word.css | 53 + tinymce/examples/custom_formats.html | 107 + tinymce/examples/full.html | 96 + tinymce/examples/index.html | 10 + tinymce/examples/lists/image_list.js | 9 + tinymce/examples/lists/link_list.js | 10 + tinymce/examples/lists/media_list.js | 10 + tinymce/examples/lists/template_list.js | 9 + tinymce/examples/media/logo.jpg | Bin 0 -> 2729 bytes tinymce/examples/media/logo_over.jpg | Bin 0 -> 6473 bytes tinymce/examples/media/sample.avi | Bin 0 -> 82944 bytes tinymce/examples/media/sample.dcr | Bin 0 -> 6774 bytes tinymce/examples/media/sample.mov | Bin 0 -> 55622 bytes tinymce/examples/media/sample.ram | 1 + tinymce/examples/media/sample.rm | Bin 0 -> 17846 bytes tinymce/examples/media/sample.swf | Bin 0 -> 6118 bytes tinymce/examples/menu.html | 17 + tinymce/examples/simple.html | 43 + tinymce/examples/skins.html | 212 + tinymce/examples/templates/layout1.htm | 15 + tinymce/examples/templates/snippet1.htm | 1 + tinymce/examples/translate.html | 80 + tinymce/examples/word.html | 67 + tinymce/jscripts/tiny_mce/langs/en.js | 170 + tinymce/jscripts/tiny_mce/license.txt | 504 + .../tiny_mce/plugins/advhr/css/advhr.css | 5 + .../tiny_mce/plugins/advhr/editor_plugin.js | 1 + .../plugins/advhr/editor_plugin_src.js | 57 + .../tiny_mce/plugins/advhr/js/rule.js | 43 + .../tiny_mce/plugins/advhr/langs/en_dlg.js | 5 + .../jscripts/tiny_mce/plugins/advhr/rule.htm | 57 + .../plugins/advimage/css/advimage.css | 13 + .../plugins/advimage/editor_plugin.js | 1 + .../plugins/advimage/editor_plugin_src.js | 50 + .../tiny_mce/plugins/advimage/image.htm | 232 + .../tiny_mce/plugins/advimage/img/sample.gif | Bin 0 -> 1624 bytes .../tiny_mce/plugins/advimage/js/image.js | 443 + .../tiny_mce/plugins/advimage/langs/en_dlg.js | 43 + .../tiny_mce/plugins/advlink/css/advlink.css | 8 + .../tiny_mce/plugins/advlink/editor_plugin.js | 1 + .../plugins/advlink/editor_plugin_src.js | 61 + .../tiny_mce/plugins/advlink/js/advlink.js | 528 + .../tiny_mce/plugins/advlink/langs/en_dlg.js | 52 + .../tiny_mce/plugins/advlink/link.htm | 333 + .../tiny_mce/plugins/advlist/editor_plugin.js | 1 + .../plugins/advlist/editor_plugin_src.js | 154 + .../plugins/autoresize/editor_plugin.js | 1 + .../plugins/autoresize/editor_plugin_src.js | 119 + .../plugins/autosave/editor_plugin.js | 1 + .../plugins/autosave/editor_plugin_src.js | 422 + .../tiny_mce/plugins/autosave/langs/en.js | 4 + .../tiny_mce/plugins/bbcode/editor_plugin.js | 1 + .../plugins/bbcode/editor_plugin_src.js | 120 + .../plugins/contextmenu/editor_plugin.js | 1 + .../plugins/contextmenu/editor_plugin_src.js | 147 + .../plugins/directionality/editor_plugin.js | 1 + .../directionality/editor_plugin_src.js | 82 + .../plugins/emotions/editor_plugin.js | 1 + .../plugins/emotions/editor_plugin_src.js | 43 + .../tiny_mce/plugins/emotions/emotions.htm | 40 + .../plugins/emotions/img/smiley-cool.gif | Bin 0 -> 354 bytes .../plugins/emotions/img/smiley-cry.gif | Bin 0 -> 329 bytes .../emotions/img/smiley-embarassed.gif | Bin 0 -> 331 bytes .../emotions/img/smiley-foot-in-mouth.gif | Bin 0 -> 344 bytes .../plugins/emotions/img/smiley-frown.gif | Bin 0 -> 340 bytes .../plugins/emotions/img/smiley-innocent.gif | Bin 0 -> 336 bytes .../plugins/emotions/img/smiley-kiss.gif | Bin 0 -> 338 bytes .../plugins/emotions/img/smiley-laughing.gif | Bin 0 -> 344 bytes .../emotions/img/smiley-money-mouth.gif | Bin 0 -> 321 bytes .../plugins/emotions/img/smiley-sealed.gif | Bin 0 -> 325 bytes .../plugins/emotions/img/smiley-smile.gif | Bin 0 -> 345 bytes .../plugins/emotions/img/smiley-surprised.gif | Bin 0 -> 342 bytes .../emotions/img/smiley-tongue-out.gif | Bin 0 -> 328 bytes .../plugins/emotions/img/smiley-undecided.gif | Bin 0 -> 337 bytes .../plugins/emotions/img/smiley-wink.gif | Bin 0 -> 351 bytes .../plugins/emotions/img/smiley-yell.gif | Bin 0 -> 336 bytes .../tiny_mce/plugins/emotions/js/emotions.js | 22 + .../tiny_mce/plugins/emotions/langs/en_dlg.js | 20 + .../tiny_mce/plugins/example/dialog.htm | 22 + .../tiny_mce/plugins/example/editor_plugin.js | 1 + .../plugins/example/editor_plugin_src.js | 84 + .../tiny_mce/plugins/example/img/example.gif | Bin 0 -> 87 bytes .../tiny_mce/plugins/example/js/dialog.js | 19 + .../tiny_mce/plugins/example/langs/en.js | 3 + .../tiny_mce/plugins/example/langs/en_dlg.js | 3 + .../plugins/fullpage/css/fullpage.css | 182 + .../plugins/fullpage/editor_plugin.js | 1 + .../plugins/fullpage/editor_plugin_src.js | 153 + .../tiny_mce/plugins/fullpage/fullpage.htm | 571 + .../tiny_mce/plugins/fullpage/js/fullpage.js | 471 + .../tiny_mce/plugins/fullpage/langs/en_dlg.js | 85 + .../plugins/fullscreen/editor_plugin.js | 1 + .../plugins/fullscreen/editor_plugin_src.js | 151 + .../plugins/fullscreen/fullscreen.htm | 109 + .../tiny_mce/plugins/iespell/editor_plugin.js | 1 + .../plugins/iespell/editor_plugin_src.js | 54 + .../plugins/inlinepopups/editor_plugin.js | 1 + .../plugins/inlinepopups/editor_plugin_src.js | 635 + .../skins/clearlooks2/img/alert.gif | Bin 0 -> 818 bytes .../skins/clearlooks2/img/button.gif | Bin 0 -> 280 bytes .../skins/clearlooks2/img/buttons.gif | Bin 0 -> 1195 bytes .../skins/clearlooks2/img/confirm.gif | Bin 0 -> 915 bytes .../skins/clearlooks2/img/corners.gif | Bin 0 -> 911 bytes .../skins/clearlooks2/img/horizontal.gif | Bin 0 -> 769 bytes .../skins/clearlooks2/img/vertical.gif | Bin 0 -> 92 bytes .../inlinepopups/skins/clearlooks2/window.css | 90 + .../plugins/inlinepopups/template.htm | 387 + .../plugins/insertdatetime/editor_plugin.js | 1 + .../insertdatetime/editor_plugin_src.js | 83 + .../tiny_mce/plugins/layer/editor_plugin.js | 1 + .../plugins/layer/editor_plugin_src.js | 212 + .../plugins/legacyoutput/editor_plugin.js | 1 + .../plugins/legacyoutput/editor_plugin_src.js | 136 + .../tiny_mce/plugins/media/css/content.css | 6 + .../tiny_mce/plugins/media/css/media.css | 16 + .../tiny_mce/plugins/media/editor_plugin.js | 1 + .../plugins/media/editor_plugin_src.js | 414 + .../tiny_mce/plugins/media/img/flash.gif | Bin 0 -> 241 bytes .../tiny_mce/plugins/media/img/flv_player.swf | Bin 0 -> 11668 bytes .../tiny_mce/plugins/media/img/quicktime.gif | Bin 0 -> 303 bytes .../tiny_mce/plugins/media/img/realmedia.gif | Bin 0 -> 439 bytes .../tiny_mce/plugins/media/img/shockwave.gif | Bin 0 -> 387 bytes .../tiny_mce/plugins/media/img/trans.gif | Bin 0 -> 43 bytes .../plugins/media/img/windowsmedia.gif | Bin 0 -> 415 bytes .../tiny_mce/plugins/media/js/embed.js | 73 + .../tiny_mce/plugins/media/js/media.js | 630 + .../tiny_mce/plugins/media/langs/en_dlg.js | 103 + .../jscripts/tiny_mce/plugins/media/media.htm | 817 + .../plugins/nonbreaking/editor_plugin.js | 1 + .../plugins/nonbreaking/editor_plugin_src.js | 53 + .../plugins/noneditable/editor_plugin.js | 1 + .../plugins/noneditable/editor_plugin_src.js | 90 + .../plugins/pagebreak/css/content.css | 1 + .../plugins/pagebreak/editor_plugin.js | 1 + .../plugins/pagebreak/editor_plugin_src.js | 77 + .../plugins/pagebreak/img/pagebreak.gif | Bin 0 -> 325 bytes .../tiny_mce/plugins/pagebreak/img/trans.gif | Bin 0 -> 43 bytes .../tiny_mce/plugins/paste/editor_plugin.js | 1 + .../plugins/paste/editor_plugin_src.js | 940 + .../tiny_mce/plugins/paste/js/pastetext.js | 36 + .../tiny_mce/plugins/paste/js/pasteword.js | 51 + .../tiny_mce/plugins/paste/langs/en_dlg.js | 5 + .../tiny_mce/plugins/paste/pastetext.htm | 27 + .../tiny_mce/plugins/paste/pasteword.htm | 21 + .../tiny_mce/plugins/preview/editor_plugin.js | 1 + .../plugins/preview/editor_plugin_src.js | 53 + .../tiny_mce/plugins/preview/example.html | 28 + .../plugins/preview/jscripts/embed.js | 73 + .../tiny_mce/plugins/preview/preview.html | 17 + .../tiny_mce/plugins/print/editor_plugin.js | 1 + .../plugins/print/editor_plugin_src.js | 34 + .../tiny_mce/plugins/save/editor_plugin.js | 1 + .../plugins/save/editor_plugin_src.js | 101 + .../searchreplace/css/searchreplace.css | 6 + .../plugins/searchreplace/editor_plugin.js | 1 + .../searchreplace/editor_plugin_src.js | 57 + .../plugins/searchreplace/js/searchreplace.js | 130 + .../plugins/searchreplace/langs/en_dlg.js | 16 + .../plugins/searchreplace/searchreplace.htm | 99 + .../plugins/spellchecker/css/content.css | 1 + .../plugins/spellchecker/editor_plugin.js | 1 + .../plugins/spellchecker/editor_plugin_src.js | 417 + .../plugins/spellchecker/img/wline.gif | Bin 0 -> 46 bytes .../tiny_mce/plugins/style/css/props.css | 13 + .../tiny_mce/plugins/style/editor_plugin.js | 1 + .../plugins/style/editor_plugin_src.js | 55 + .../tiny_mce/plugins/style/js/props.js | 641 + .../tiny_mce/plugins/style/langs/en_dlg.js | 63 + .../jscripts/tiny_mce/plugins/style/props.htm | 723 + .../plugins/tabfocus/editor_plugin.js | 1 + .../plugins/tabfocus/editor_plugin_src.js | 112 + .../jscripts/tiny_mce/plugins/table/cell.htm | 178 + .../tiny_mce/plugins/table/css/cell.css | 17 + .../tiny_mce/plugins/table/css/row.css | 25 + .../tiny_mce/plugins/table/css/table.css | 13 + .../tiny_mce/plugins/table/editor_plugin.js | 1 + .../plugins/table/editor_plugin_src.js | 1125 ++ .../tiny_mce/plugins/table/js/cell.js | 286 + .../tiny_mce/plugins/table/js/merge_cells.js | 27 + .../jscripts/tiny_mce/plugins/table/js/row.js | 237 + .../tiny_mce/plugins/table/js/table.js | 449 + .../tiny_mce/plugins/table/langs/en_dlg.js | 74 + .../tiny_mce/plugins/table/merge_cells.htm | 32 + .../jscripts/tiny_mce/plugins/table/row.htm | 155 + .../jscripts/tiny_mce/plugins/table/table.htm | 187 + .../tiny_mce/plugins/template/blank.htm | 12 + .../plugins/template/css/template.css | 23 + .../plugins/template/editor_plugin.js | 1 + .../plugins/template/editor_plugin_src.js | 159 + .../tiny_mce/plugins/template/js/template.js | 106 + .../tiny_mce/plugins/template/langs/en_dlg.js | 15 + .../tiny_mce/plugins/template/template.htm | 31 + .../plugins/visualchars/editor_plugin.js | 1 + .../plugins/visualchars/editor_plugin_src.js | 76 + .../plugins/wordcount/editor_plugin.js | 1 + .../plugins/wordcount/editor_plugin_src.js | 98 + .../tiny_mce/plugins/xhtmlxtras/abbr.htm | 141 + .../tiny_mce/plugins/xhtmlxtras/acronym.htm | 141 + .../plugins/xhtmlxtras/attributes.htm | 148 + .../tiny_mce/plugins/xhtmlxtras/cite.htm | 141 + .../plugins/xhtmlxtras/css/attributes.css | 11 + .../tiny_mce/plugins/xhtmlxtras/css/popup.css | 9 + .../tiny_mce/plugins/xhtmlxtras/del.htm | 161 + .../plugins/xhtmlxtras/editor_plugin.js | 1 + .../plugins/xhtmlxtras/editor_plugin_src.js | 144 + .../tiny_mce/plugins/xhtmlxtras/ins.htm | 161 + .../tiny_mce/plugins/xhtmlxtras/js/abbr.js | 28 + .../tiny_mce/plugins/xhtmlxtras/js/acronym.js | 28 + .../plugins/xhtmlxtras/js/attributes.js | 126 + .../tiny_mce/plugins/xhtmlxtras/js/cite.js | 28 + .../tiny_mce/plugins/xhtmlxtras/js/del.js | 63 + .../plugins/xhtmlxtras/js/element_common.js | 231 + .../tiny_mce/plugins/xhtmlxtras/js/ins.js | 62 + .../plugins/xhtmlxtras/langs/en_dlg.js | 32 + .../tiny_mce/themes/advanced/about.htm | 54 + .../tiny_mce/themes/advanced/anchor.htm | 26 + .../tiny_mce/themes/advanced/charmap.htm | 52 + .../tiny_mce/themes/advanced/color_picker.htm | 73 + .../themes/advanced/editor_template.js | 1 + .../themes/advanced/editor_template_src.js | 1194 ++ .../tiny_mce/themes/advanced/image.htm | 80 + .../themes/advanced/img/colorpicker.jpg | Bin 0 -> 3189 bytes .../tiny_mce/themes/advanced/img/icons.gif | Bin 0 -> 11794 bytes .../tiny_mce/themes/advanced/js/about.js | 72 + .../tiny_mce/themes/advanced/js/anchor.js | 37 + .../tiny_mce/themes/advanced/js/charmap.js | 335 + .../themes/advanced/js/color_picker.js | 253 + .../tiny_mce/themes/advanced/js/image.js | 245 + .../tiny_mce/themes/advanced/js/link.js | 156 + .../themes/advanced/js/source_editor.js | 62 + .../tiny_mce/themes/advanced/langs/en.js | 62 + .../tiny_mce/themes/advanced/langs/en_dlg.js | 51 + .../tiny_mce/themes/advanced/link.htm | 58 + .../themes/advanced/skins/default/content.css | 35 + .../themes/advanced/skins/default/dialog.css | 117 + .../advanced/skins/default/img/buttons.png | Bin 0 -> 3274 bytes .../advanced/skins/default/img/items.gif | Bin 0 -> 70 bytes .../advanced/skins/default/img/menu_arrow.gif | Bin 0 -> 68 bytes .../advanced/skins/default/img/menu_check.gif | Bin 0 -> 70 bytes .../advanced/skins/default/img/progress.gif | Bin 0 -> 1787 bytes .../advanced/skins/default/img/tabs.gif | Bin 0 -> 1326 bytes .../themes/advanced/skins/default/ui.css | 213 + .../themes/advanced/skins/o2k7/content.css | 35 + .../themes/advanced/skins/o2k7/dialog.css | 116 + .../advanced/skins/o2k7/img/button_bg.png | Bin 0 -> 5859 bytes .../skins/o2k7/img/button_bg_black.png | Bin 0 -> 3736 bytes .../skins/o2k7/img/button_bg_silver.png | Bin 0 -> 5358 bytes .../themes/advanced/skins/o2k7/ui.css | 215 + .../themes/advanced/skins/o2k7/ui_black.css | 8 + .../themes/advanced/skins/o2k7/ui_silver.css | 5 + .../themes/advanced/source_editor.htm | 25 + .../tiny_mce/themes/simple/editor_template.js | 1 + .../themes/simple/editor_template_src.js | 85 + .../tiny_mce/themes/simple/img/icons.gif | Bin 0 -> 1440 bytes .../tiny_mce/themes/simple/langs/en.js | 11 + .../themes/simple/skins/default/content.css | 25 + .../themes/simple/skins/default/ui.css | 32 + .../themes/simple/skins/o2k7/content.css | 17 + .../simple/skins/o2k7/img/button_bg.png | Bin 0 -> 5102 bytes .../tiny_mce/themes/simple/skins/o2k7/ui.css | 35 + tinymce/jscripts/tiny_mce/tiny_mce.js | 1 + tinymce/jscripts/tiny_mce/tiny_mce_popup.js | 5 + tinymce/jscripts/tiny_mce/tiny_mce_src.js | 14198 ++++++++++++++++ .../tiny_mce/utils/editable_selects.js | 70 + tinymce/jscripts/tiny_mce/utils/form_utils.js | 200 + tinymce/jscripts/tiny_mce/utils/mctabs.js | 77 + tinymce/jscripts/tiny_mce/utils/validate.js | 220 + view/about.tpl | 14 + view/contact_selectors.php | 21 + view/contact_self.tpl | 9 + view/contact_template.tpl | 16 + view/contacts-top.tpl | 5 + view/cropbody.tpl | 56 + view/crophead.tpl | 6 + view/custom_tinymce.css | 35 + view/default.php | 15 + view/dfrn_req_confirm.tpl | 17 + view/dfrn_request.tpl | 54 + view/head.tpl | 8 + view/intro_complete_eml.tpl | 28 + view/intros-top.tpl | 7 + view/intros.tpl | 21 + view/jot-header.tpl | 35 + view/jot-plain.tpl | 15 + view/jot-save.tpl | 31 + view/jot.tpl | 18 + view/login.tpl | 25 + view/logout.tpl | 6 + view/profile-in-directory.tpl | 16 + view/profile.php | 66 + view/profile_edit.tpl | 91 + view/profile_entry.tpl | 13 + view/profile_entry_default.tpl | 11 + view/profile_listing_header.tpl | 8 + view/profile_photo.tpl | 14 + view/profile_selectors.php | 32 + view/register-link.tpl | 1 + view/register.tpl | 15 + view/register_open_eml.tpl | 18 + view/settings.tpl | 49 + view/settings_nick_set.tpl | 8 + view/settings_nick_unset.tpl | 13 + view/sidenote.tpl | 17 + view/silho.gif | Bin 0 -> 79 bytes view/style.css | 421 + view/wall_item.tpl | 14 + wip/bbcodemaster | 159 + wip/country_state_selector_ajax1.5.5.zip | Bin 0 -> 58312 bytes wip/countrylist.php | 257 + 393 files changed, 59765 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 auth.php create mode 100644 boot.php create mode 100644 cropper/copper.html create mode 100644 cropper/cropper.css create mode 100644 cropper/cropper.js create mode 100644 cropper/cropper.uncompressed.js create mode 100644 cropper/lib/builder.js create mode 100644 cropper/lib/controls.js create mode 100644 cropper/lib/dragdrop.js create mode 100644 cropper/lib/effects.js create mode 100644 cropper/lib/prototype.js create mode 100644 cropper/lib/scriptaculous.js create mode 100644 cropper/lib/slider.js create mode 100644 cropper/lib/unittest.js create mode 100644 cropper/licence.txt create mode 100644 cropper/marqueeHoriz.gif create mode 100644 cropper/marqueeVert.gif create mode 100644 cropper/tests/castle.jpg create mode 100644 cropper/tests/castleMed.jpg create mode 100644 cropper/tests/example-Basic.htm create mode 100644 cropper/tests/example-CSS-Absolute.htm create mode 100644 cropper/tests/example-CSS-Float.htm create mode 100644 cropper/tests/example-CSS-Relative.htm create mode 100644 cropper/tests/example-CoordsOnLoad.htm create mode 100644 cropper/tests/example-CoordsOnLoadWithRatio.htm create mode 100644 cropper/tests/example-Dimensions.htm create mode 100644 cropper/tests/example-DynamicImage.htm create mode 100644 cropper/tests/example-FixedRatio.htm create mode 100644 cropper/tests/example-MinimumDimensions.htm create mode 100644 cropper/tests/example-MinimumWidth.htm create mode 100644 cropper/tests/example-Preview.htm create mode 100644 cropper/tests/poppy.jpg create mode 100644 cropper/tests/staticHTMLStructure.htm create mode 100644 favicon.gif create mode 100644 favicon.ico create mode 100644 images/b_block.gif create mode 100644 images/b_drop.gif create mode 100644 images/b_drop.png create mode 100644 images/b_edit.gif create mode 100644 images/b_edit.png create mode 100644 images/default-profile-sm.jpg create mode 100644 images/default-profile.jpg create mode 100644 images/larrw.gif create mode 100644 images/rarrw.gif create mode 100644 include/Photo.php create mode 100644 include/Scrape.php create mode 100644 include/bbcode.php create mode 100644 include/datetime.php create mode 100644 include/dba.php create mode 100644 include/login.php create mode 100644 include/security.php create mode 100644 include/session.php create mode 100644 include/system_unavailable.php create mode 100644 index.php create mode 100644 library/HTML5/Data.php create mode 100644 library/HTML5/InputStream.php create mode 100644 library/HTML5/Parser.php create mode 100644 library/HTML5/Tokenizer.php create mode 100644 library/HTML5/TreeBuilder.php create mode 100644 library/HTML5/named-character-references.ser create mode 100644 mod/contacts.php create mode 100644 mod/dfrn_confirm.php create mode 100644 mod/dfrn_poll.php create mode 100644 mod/dfrn_request.php create mode 100644 mod/home.php create mode 100644 mod/item.php create mode 100644 mod/login.php create mode 100644 mod/notifications.php create mode 100644 mod/photo.php create mode 100644 mod/profile.php create mode 100644 mod/profile_photo.php create mode 100644 mod/profiles.php create mode 100644 mod/redir.php create mode 100644 mod/register.php create mode 100644 mod/settings.php create mode 100644 mod/test.php create mode 100644 nav.php create mode 100644 robots.txt create mode 100644 silho.gif create mode 100644 silho.ico create mode 100644 tinymce/changelog.txt create mode 100644 tinymce/examples/css/content.css create mode 100644 tinymce/examples/css/word.css create mode 100644 tinymce/examples/custom_formats.html create mode 100644 tinymce/examples/full.html create mode 100644 tinymce/examples/index.html create mode 100644 tinymce/examples/lists/image_list.js create mode 100644 tinymce/examples/lists/link_list.js create mode 100644 tinymce/examples/lists/media_list.js create mode 100644 tinymce/examples/lists/template_list.js create mode 100644 tinymce/examples/media/logo.jpg create mode 100644 tinymce/examples/media/logo_over.jpg create mode 100644 tinymce/examples/media/sample.avi create mode 100644 tinymce/examples/media/sample.dcr create mode 100644 tinymce/examples/media/sample.mov create mode 100644 tinymce/examples/media/sample.ram create mode 100644 tinymce/examples/media/sample.rm create mode 100644 tinymce/examples/media/sample.swf create mode 100644 tinymce/examples/menu.html create mode 100644 tinymce/examples/simple.html create mode 100644 tinymce/examples/skins.html create mode 100644 tinymce/examples/templates/layout1.htm create mode 100644 tinymce/examples/templates/snippet1.htm create mode 100644 tinymce/examples/translate.html create mode 100644 tinymce/examples/word.html create mode 100644 tinymce/jscripts/tiny_mce/langs/en.js create mode 100644 tinymce/jscripts/tiny_mce/license.txt create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/css/advhr.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/js/rule.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advhr/rule.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/css/advimage.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/image.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/img/sample.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/js/image.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advimage/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/css/advlink.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/js/advlink.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlink/link.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlist/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/advlist/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/autoresize/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/autoresize/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/autosave/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/autosave/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/autosave/langs/en.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/bbcode/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/bbcode/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/contextmenu/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/contextmenu/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/directionality/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/directionality/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/emotions.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-cool.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-cry.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-embarassed.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-foot-in-mouth.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-frown.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-innocent.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-kiss.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-laughing.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-money-mouth.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-sealed.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-smile.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-surprised.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-tongue-out.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-undecided.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-wink.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/img/smiley-yell.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/js/emotions.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/emotions/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/dialog.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/img/example.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/js/dialog.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/langs/en.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/example/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/css/fullpage.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/fullpage.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/js/fullpage.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullpage/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullscreen/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullscreen/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/fullscreen/fullscreen.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/iespell/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/iespell/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/alert.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/button.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/buttons.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/confirm.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/corners.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/horizontal.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/img/vertical.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/skins/clearlooks2/window.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/inlinepopups/template.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/insertdatetime/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/insertdatetime/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/layer/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/layer/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/legacyoutput/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/legacyoutput/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/css/content.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/css/media.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/flash.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/flv_player.swf create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/quicktime.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/realmedia.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/shockwave.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/trans.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/img/windowsmedia.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/js/embed.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/js/media.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/media/media.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/nonbreaking/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/nonbreaking/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/noneditable/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/noneditable/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/pagebreak/css/content.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/pagebreak/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/pagebreak/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/pagebreak/img/pagebreak.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/pagebreak/img/trans.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/js/pastetext.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/js/pasteword.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/pastetext.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/paste/pasteword.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/preview/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/preview/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/preview/example.html create mode 100644 tinymce/jscripts/tiny_mce/plugins/preview/jscripts/embed.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/preview/preview.html create mode 100644 tinymce/jscripts/tiny_mce/plugins/print/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/print/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/save/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/save/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/css/searchreplace.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/js/searchreplace.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/searchreplace/searchreplace.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/spellchecker/css/content.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/spellchecker/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/spellchecker/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/spellchecker/img/wline.gif create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/css/props.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/js/props.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/style/props.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/tabfocus/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/tabfocus/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/cell.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/css/cell.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/css/row.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/css/table.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/js/cell.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/js/merge_cells.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/js/row.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/js/table.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/merge_cells.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/row.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/table/table.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/blank.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/css/template.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/js/template.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/template/template.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/visualchars/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/visualchars/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/wordcount/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/wordcount/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/abbr.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/acronym.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/attributes.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/cite.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/css/attributes.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/css/popup.css create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/del.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/editor_plugin.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/editor_plugin_src.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/ins.htm create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/abbr.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/acronym.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/attributes.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/cite.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/del.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/element_common.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/js/ins.js create mode 100644 tinymce/jscripts/tiny_mce/plugins/xhtmlxtras/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/about.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/anchor.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/charmap.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/color_picker.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/editor_template.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/editor_template_src.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/image.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/img/colorpicker.jpg create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/img/icons.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/about.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/anchor.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/charmap.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/color_picker.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/image.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/link.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/js/source_editor.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/langs/en.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/langs/en_dlg.js create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/link.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/content.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/dialog.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/buttons.png create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/items.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/menu_arrow.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/menu_check.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/progress.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/img/tabs.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/default/ui.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/content.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/dialog.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/img/button_bg.png create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_black.png create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/img/button_bg_silver.png create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/ui.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/ui_black.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css create mode 100644 tinymce/jscripts/tiny_mce/themes/advanced/source_editor.htm create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/editor_template.js create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/editor_template_src.js create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/img/icons.gif create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/langs/en.js create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/skins/default/content.css create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/skins/default/ui.css create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/skins/o2k7/content.css create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/skins/o2k7/img/button_bg.png create mode 100644 tinymce/jscripts/tiny_mce/themes/simple/skins/o2k7/ui.css create mode 100644 tinymce/jscripts/tiny_mce/tiny_mce.js create mode 100644 tinymce/jscripts/tiny_mce/tiny_mce_popup.js create mode 100644 tinymce/jscripts/tiny_mce/tiny_mce_src.js create mode 100644 tinymce/jscripts/tiny_mce/utils/editable_selects.js create mode 100644 tinymce/jscripts/tiny_mce/utils/form_utils.js create mode 100644 tinymce/jscripts/tiny_mce/utils/mctabs.js create mode 100644 tinymce/jscripts/tiny_mce/utils/validate.js create mode 100644 view/about.tpl create mode 100644 view/contact_selectors.php create mode 100644 view/contact_self.tpl create mode 100644 view/contact_template.tpl create mode 100644 view/contacts-top.tpl create mode 100644 view/cropbody.tpl create mode 100644 view/crophead.tpl create mode 100644 view/custom_tinymce.css create mode 100644 view/default.php create mode 100644 view/dfrn_req_confirm.tpl create mode 100644 view/dfrn_request.tpl create mode 100644 view/head.tpl create mode 100644 view/intro_complete_eml.tpl create mode 100644 view/intros-top.tpl create mode 100644 view/intros.tpl create mode 100644 view/jot-header.tpl create mode 100644 view/jot-plain.tpl create mode 100644 view/jot-save.tpl create mode 100644 view/jot.tpl create mode 100644 view/login.tpl create mode 100644 view/logout.tpl create mode 100644 view/profile-in-directory.tpl create mode 100644 view/profile.php create mode 100644 view/profile_edit.tpl create mode 100644 view/profile_entry.tpl create mode 100644 view/profile_entry_default.tpl create mode 100644 view/profile_listing_header.tpl create mode 100644 view/profile_photo.tpl create mode 100644 view/profile_selectors.php create mode 100644 view/register-link.tpl create mode 100644 view/register.tpl create mode 100644 view/register_open_eml.tpl create mode 100644 view/settings.tpl create mode 100644 view/settings_nick_set.tpl create mode 100644 view/settings_nick_unset.tpl create mode 100644 view/sidenote.tpl create mode 100644 view/silho.gif create mode 100644 view/style.css create mode 100644 view/wall_item.tpl create mode 100644 wip/bbcodemaster create mode 100644 wip/country_state_selector_ajax1.5.5.zip create mode 100644 wip/countrylist.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e85d828 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.htconfig.php +\#* diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..2e0e585 --- /dev/null +++ b/.htaccess @@ -0,0 +1,13 @@ + +Options -Indexes + + + RewriteEngine on + + # Rewrite current-style URLs of the form 'index.php?q=x'. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)$ index.php?q=$1 [L,QSA] + + + diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..56cea90 --- /dev/null +++ b/auth.php @@ -0,0 +1,79 @@ +module == "logout") { + unset($_SESSION['authenticated']); + unset($_SESSION['uid']); + unset($_SESSION['visitor_id']); + unset($_SESSION['administrator']); + $_SESSION['sysmsg'] = "Logged out." . EOL; + goaway($a->get_baseurl()); + } + if(x($_SESSION,'uid')) { + $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", + intval($_SESSION['uid'])); + if($r === NULL || (! count($r))) { + goaway($a->get_baseurl()); + } + $a->user = $r[0]; + if(strlen($a->user['timezone'])) + date_default_timezone_set($a->user['timezone']); + + } +} +else { + unset($_SESSION['authenticated']); + unset($_SESSION['uid']); + unset($_SESSION['visitor_id']); + unset($_SESSION['administrator']); + $encrypted = hash('whirlpool',trim($_POST['password'])); + + if((x($_POST,'auth-params')) && $_POST['auth-params'] == 'login') { + $r = q("SELECT * FROM `user` + WHERE `email` = '%s' AND `password` = '%s' LIMIT 1", + dbesc(trim($_POST['login-name'])), + dbesc($encrypted)); + if(($r === false) || (! count($r))) { + $_SESSION['sysmsg'] = 'Login failed.' . EOL ; + goaway($a->get_baseurl()); + } + $_SESSION['uid'] = $r[0]['uid']; + $_SESSION['admin'] = $r[0]['admin']; + $_SESSION['authenticated'] = 1; + if(x($r[0],'nickname')) + $_SESSION['my_url'] = $a->get_baseurl() . '/profile/' . $r[0]['nickname']; + else + $_SESSION['my_url'] = $a->get_baseurl() . '/profile/' . $r[0]['uid']; + + $_SESSION['sysmsg'] = "Welcome back " . $r[0]['username'] . EOL; + $a->user = $r[0]; + if(strlen($a->user['timezone'])) + date_default_timezone_set($a->user['timezone']); + + } +} + +// Returns an array of group names this contact is a member of. +// Since contact-id's are unique and each "belongs" to a given user uid, +// this array will only contain group names related to the uid of this +// DFRN contact. They are *not* neccessarily unique across the entire site. + + +if(! function_exists('init_groups_visitor')) { +function init_groups_visitor($contact_id) { + $groups = array(); + $r = q("SELECT `group_member`.`gid`, `group`.`name` + FROM `group_member` LEFT JOIN `group` ON `group_member`.`gid` = `group`.`id` + WHERE `group_member`.`contact-id` = %d ", + intval($contact_id) + ); + if(count($r)) { + foreach($r as $rr) + $groups[] = $rr['name']; + } + return $groups; +}} + + diff --git a/boot.php b/boot.php new file mode 100644 index 0000000..7f7a817 --- /dev/null +++ b/boot.php @@ -0,0 +1,302 @@ +'); + +define('REGISTER_CLOSED', 0); +define('REGISTER_APPROVE', 1); +define('REGISTER_OPEN', 2); + + +if(! class_exists('App')) { +class App { + + public $module_loaded = false; + public $config; + public $page; + public $profile; + public $user; + public $content; + public $error = false; + public $cmd; + public $argv; + public $argc; + public $module; + + private $scheme; + private $hostname; + private $path; + private $db; + + function __construct() { + + $this->config = array(); + $this->page = array(); + + $this->scheme = ((isset($_SERVER['HTTPS']) + && ($_SERVER['HTTPS'])) ? 'https' : 'http' ); + $this->hostname = str_replace('www.','', + $_SERVER['SERVER_NAME']); + set_include_path("include/$this->hostname" + . PATH_SEPARATOR . 'include' + . PATH_SEPARATOR . '.' ); + + if(substr($_SERVER['QUERY_STRING'],0,2) == "q=") + $_SERVER['QUERY_STRING'] = substr($_SERVER['QUERY_STRING'],2); +// $this->cmd = trim($_SERVER['QUERY_STRING'],'/'); + $this->cmd = trim($_GET['q'],'/'); + + $this->argv = explode('/',$this->cmd); + $this->argc = count($this->argv); + if((array_key_exists('0',$this->argv)) && strlen($this->argv[0])) { + $this->module = $this->argv[0]; + } + else { + $this->module = 'home'; + } + } + + function get_baseurl($ssl = false) { + + return (($ssl) ? 'https' : $this->scheme) . "://" . $this->hostname + . ((isset($this->path) && strlen($this->path)) + ? '/' . $this->path : '' ); + } + + function set_path($p) { + $this->path = ltrim(trim($p),'/'); + } + + function init_pagehead() { + if(file_exists("view/head.tpl")) + $s = file_get_contents("view/head.tpl"); + $this->page['htmlhead'] = replace_macros($s,array('$baseurl' => $this->get_baseurl())); + } + +}} + + +if(! function_exists('x')) { +function x($s,$k = NULL) { + if($k != NULL) { + if((is_array($s)) && (array_key_exists($k,$s))) { + if($s[$k]) + return (int) 1; + return (int) 0; + } + return false; + } + else { + if(isset($s)) { + if($s) { + return (int) 1; + } + return (int) 0; + } + return false; + } +}} + +if(! function_exists('system_unavailable')) { +function system_unavailable() { + include('system_unavailable.php'); + killme(); +}} + +if(! function_exists('replace_macros')) { +function replace_macros($s,$r) { + + $search = array(); + $replace = array(); + + if(is_array($r) && count($r)) { + foreach ($r as $k => $v ) { + $search[] = $k; + $replace[] = $v; + } + } + return str_replace($search,$replace,$s); +}} + + + +if(! function_exists('fetch_url')) { +function fetch_url($url,$binary = false) { + $ch = curl_init($url); + if(! $ch) return false; + + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true); + curl_setopt($ch, CURLOPT_MAXREDIRS,8); + curl_setopt($ch, CURLOPT_RETURNTRANSFER,true); + if($binary) + curl_setopt($ch, CURLOPT_BINARYTRANSFER,1); + + $s = curl_exec($ch); + curl_close($ch); + return($s); +}} + + +if(! function_exists('post_url')) { +function post_url($url,$params) { + $ch = curl_init($url); + if(! $ch) return false; + + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true); + curl_setopt($ch, CURLOPT_MAXREDIRS,8); + curl_setopt($ch, CURLOPT_RETURNTRANSFER,true); + curl_setopt($ch, CURLOPT_POST,1); + curl_setopt($ch, CURLOPT_POSTFIELDS,$params); + + $s = curl_exec($ch); + curl_close($ch); + return($s); +}} + + +if(! function_exists('random_string')) { +function random_string() { + return(hash('sha256',uniqid(rand(),true))); +}} + +if(! function_exists('notags')) { +function notags($string) { + // protect against :<> with high-bit set + return(str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string)); +}} + +// The PHP built-in tag escape function has traditionally been buggy +if(! function_exists('escape_tags')) { +function escape_tags($string) { + return(str_replace(array("<",">","&"), array('<','>','&'), $string)); +}} + +if(! function_exists('login')) { +function login($register = false) { + $o = ""; + $register_html = (($register) ? file_get_contents("view/register-link.tpl") : ""); + + + if(x($_SESSION,'authenticated')) { + $o = file_get_contents("view/logout.tpl"); + } + else { + $o = file_get_contents("view/login.tpl"); + + $o = replace_macros($o,array('$register_html' => $register_html )); + } + return $o; +}} + + +if(! function_exists('autoname')) { +function autoname($len) { + + $vowels = array('a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'); + if(mt_rand(0,5) == 4) + $vowels[] = 'y'; + + $cons = array( + 'b','bl','br', + 'c','ch','cl','cr', + 'd','dr', + 'f','fl','fr', + 'g','gh','gl','gr', + 'h', + 'j', + 'k','kh','kl','kr', + 'l', + 'm', + 'n', + 'p','ph','pl','pr', + 'qu', + 'r','rh', + 's','sc','sh','sm','sp','st', + 't','th','tr', + 'v', + 'w','wh', + 'x', + 'z','zh' + ); + + $midcons = array('ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp', + 'nd','ng','nk','nt','rn','rp','rt'); + + $noend = array('bl', 'br', 'cl','cr','dr','fl','fr','gl','gr', + 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh'); + + $start = mt_rand(0,2); + if($start == 0) + $table = $vowels; + else + $table = $cons; + + $word = ''; + + for ($x = 0; $x < $len; $x ++) { + $r = mt_rand(0,count($table) - 1); + $word .= $table[$r]; + + if($table == $vowels) + $table = array_merge($cons,$midcons); + else + $table = $vowels; + + } + + $word = substr($word,0,$len); + + foreach($noend as $noe) { + if((strlen($word) > 2) && (substr($word,-2) == $noe)) { + $word = substr($word,0,-1); + break; + } + } + if(substr($word,-1) == 'q') + $word = substr($word,0,-1); + return $word; +}} + +if(! function_exists('killme')) { +function killme() { + session_write_close(); + exit; +}} + +if(! function_exists('goaway')) { +function goaway($s) { + header("Location: $s"); + killme(); +}} + + +if(! function_exists('xml_status')) { +function xml_status($st) { + header( "Content-type: text/xml"); + echo ''."\r\n"; + echo "$st\r\n"; + killme(); +}} + +if(! function_exists('local_user')) { +function local_user() { + if((x($_SESSION,'authenticated')) && (x($_SESSION,'uid'))) + return $_SESSION['uid']; + return false; +}} + +if(! function_exists('remote_user')) { +function remote_user() { + if((x($_SESSION,'authenticated')) && (x($_SESSION,'cid'))) + return $_SESSION['cid']; + return false; +}} + +function footer(&$a) { + + $s = fetch_url("http://fortunemod.com/cookie.php?equal=1"); + $a->page['content'] .= "
$s
"; +} \ No newline at end of file diff --git a/cropper/copper.html b/cropper/copper.html new file mode 100644 index 0000000..6e3f09e --- /dev/null +++ b/cropper/copper.html @@ -0,0 +1,227 @@ + 1. + + 2. + + 3. + + +Options + +ratioDim obj + The pixel dimensions to apply as a restrictive ratio, with properties x & y. +minWidth int + The minimum width for the select area in pixels. +minHeight int + The mimimum height for the select area in pixels. +maxWidth int + The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed) +maxHeight int + The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed) +displayOnInit int + Whether to display the select area on initialisation, only used when providing minimum width & height or ratio. +onEndCrop func + The callback function to provide the crop details to on end of a crop. +captureKeys boolean + Whether to capture the keys for moving the select area, as these can cause some problems at the moment. +onloadCoords obj + A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload + +The callback function + +The callback function is a function that allows you to capture the crop co-ordinates when the user finished a crop movement, it is passed two arguments: + + * coords, obj, coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area. + * dimensions, obj, dimensions object with properities width & height; for the dimensions of the select area. + +An example function which outputs the crop values to form fields: +Display code as plain text +JavaScript: + + 1. + function onEndCrop( coords, dimensions ) { + 2. + $( 'x1' ).value = coords.x1; + 3. + $( 'y1' ).value = coords.y1; + 4. + $( 'x2' ).value = coords.x2; + 5. + $( 'y2' ).value = coords.y2; + 6. + $( 'width' ).value = dimensions.width; + 7. + $( 'height' ).value = dimensions.height; + 8. + } + +Basic interface + +This basic example will attach the cropper UI to the test image and return crop results to the provided callback function. +Display code as plain text +HTML: + + 1. + Test image + 2. + + 3. + + +Minimum dimensions + +You can apply minimum dimensions to a single axis or both, this example applies minimum dimensions to both axis. +Display code as plain text +HTML: + + 1. + Test image + 2. + + 3. + + +Select area ratio + +You can apply a ratio to the selection area, this example applies a 4:3 ratio to the select area. +Display code as plain text +HTML: + + 1. + Test image + 2. + + 3. + + +With crop preview + +You can display a dynamically prouced preview of the resulting crop by using the ImgWithPreview subclass, a preview can only be displayed when we have a fixed size (set via minWidth & minHeight options). Note that the displayOnInit option is not required as this is the default behaviour when displaying a crop preview. +Display code as plain text +HTML: + + 1. + Test image + 2. +
+ 3. + + 4. + + +Known Issues + + * Safari animated gifs, only one of each will animate, this seems to be a known Safari issue. + * After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height appears as the last height until the user drags, this appears to be the related to another IE error (which has been fixed) where IE does not always redraw the select area properly. + * Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, if Opera 8 support is important you & you want the overlay to work then you can use the Opera rules in the CSS to apply a black PNG with 50% alpha transparency to replicate the effect. + * Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended. + * overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera when applied (maybe Mac browsers too) I'm not sure why yet. + +If you use CakePHP you will notice that including this in your script will break the CSS layout. This is due to the CSS rule + +form div{ +vertical-align: text-top; +margin-left: 1em; +margin-bottom:2em; +overflow: auto; +} + +A simple workaround is to add another rule directly after this like so: + +form div.no_cake, form div.no_cake div { +margin:0; +overflow:hidden; +} + +and then in your code surround the img tag with a div with the class name of no_cake. + +Cheers \ No newline at end of file diff --git a/cropper/cropper.css b/cropper/cropper.css new file mode 100644 index 0000000..c2e7598 --- /dev/null +++ b/cropper/cropper.css @@ -0,0 +1,182 @@ +.imgCrop_wrap { + /* width: 500px; @done_in_js */ + /* height: 375px; @done_in_js */ + position: relative; + cursor: crosshair; +} + +/* an extra classname is applied for Opera < 9.0 to fix it's lack of opacity support */ +.imgCrop_wrap.opera8 .imgCrop_overlay, +.imgCrop_wrap.opera8 .imgCrop_clickArea { + background-color: transparent; +} + +/* fix for IE displaying all boxes at line-height by default, although they are still 1 pixel high until we combine them with the pointless span */ +.imgCrop_wrap, +.imgCrop_wrap * { + font-size: 0; +} + +.imgCrop_overlay { + background-color: #000; + opacity: 0.5; + filter:alpha(opacity=50); + position: absolute; + width: 100%; + height: 100%; +} + +.imgCrop_selArea { + position: absolute; + /* @done_in_js + top: 20px; + left: 20px; + width: 200px; + height: 200px; + background: transparent url(castle.jpg) no-repeat -210px -110px; + */ + cursor: move; + z-index: 2; +} + +/* clickArea is all a fix for IE 5.5 & 6 to allow the user to click on the given area */ +.imgCrop_clickArea { + width: 100%; + height: 100%; + background-color: #FFF; + opacity: 0.01; + filter:alpha(opacity=01); +} + +.imgCrop_marqueeHoriz { + position: absolute; + width: 100%; + height: 1px; + background: transparent url(marqueeHoriz.gif) repeat-x 0 0; + z-index: 3; +} + +.imgCrop_marqueeVert { + position: absolute; + height: 100%; + width: 1px; + background: transparent url(marqueeVert.gif) repeat-y 0 0; + z-index: 3; +} + +/* + * FIX MARCHING ANTS IN IE + * As IE <6 tries to load background images we can uncomment the follwoing hack + * to remove that issue, not as pretty - but is anything in IE? + * And yes I do know that 'filter' is evil, but it will make it look semi decent in IE + * +* html .imgCrop_marqueeHoriz, +* html .imgCrop_marqueeVert { + background: transparent; + filter: Invert; +} +* html .imgCrop_marqueeNorth { border-top: 1px dashed #000; } +* html .imgCrop_marqueeEast { border-right: 1px dashed #000; } +* html .imgCrop_marqueeSouth { border-bottom: 1px dashed #000; } +* html .imgCrop_marqueeWest { border-left: 1px dashed #000; } +*/ + +.imgCrop_marqueeNorth { top: 0; left: 0; } +.imgCrop_marqueeEast { top: 0; right: 0; } +.imgCrop_marqueeSouth { bottom: 0px; left: 0; } +.imgCrop_marqueeWest { top: 0; left: 0; } + + +.imgCrop_handle { + position: absolute; + border: 1px solid #333; + width: 6px; + height: 6px; + background: #FFF; + opacity: 0.5; + filter:alpha(opacity=50); + z-index: 4; +} + +/* fix IE 5 box model */ +* html .imgCrop_handle { + width: 8px; + height: 8px; + wid\th: 6px; + hei\ght: 6px; +} + +.imgCrop_handleN { + top: -3px; + left: 0; + /* margin-left: 49%; @done_in_js */ + cursor: n-resize; +} + +.imgCrop_handleNE { + top: -3px; + right: -3px; + cursor: ne-resize; +} + +.imgCrop_handleE { + top: 0; + right: -3px; + /* margin-top: 49%; @done_in_js */ + cursor: e-resize; +} + +.imgCrop_handleSE { + right: -3px; + bottom: -3px; + cursor: se-resize; +} + +.imgCrop_handleS { + right: 0; + bottom: -3px; + /* margin-right: 49%; @done_in_js */ + cursor: s-resize; +} + +.imgCrop_handleSW { + left: -3px; + bottom: -3px; + cursor: sw-resize; +} + +.imgCrop_handleW { + top: 0; + left: -3px; + /* margin-top: 49%; @done_in_js */ + cursor: w-resize; +} + +.imgCrop_handleNW { + top: -3px; + left: -3px; + cursor: nw-resize; +} + +/** + * Create an area to click & drag around on as the default browser behaviour is to let you drag the image + */ +.imgCrop_dragArea { + width: 100%; + height: 100%; + z-index: 200; + position: absolute; + top: 0; + left: 0; +} + +.imgCrop_previewWrap { + /* width: 200px; @done_in_js */ + /* height: 200px; @done_in_js */ + overflow: hidden; + position: relative; +} + +.imgCrop_previewWrap img { + position: absolute; +} \ No newline at end of file diff --git a/cropper/cropper.js b/cropper/cropper.js new file mode 100644 index 0000000..486a92a --- /dev/null +++ b/cropper/cropper.js @@ -0,0 +1,566 @@ +/** + * Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/) + * 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 David Spurr 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. + * + * http://www.opensource.org/licenses/bsd-license.php + * + * See scriptaculous.js for full scriptaculous licence + */ + +var CropDraggable=Class.create(); +Object.extend(Object.extend(CropDraggable.prototype,Draggable.prototype),{initialize:function(_1){ +this.options=Object.extend({drawMethod:function(){ +}},arguments[1]||{}); +this.element=$(_1); +this.handle=this.element; +this.delta=this.currentDelta(); +this.dragging=false; +this.eventMouseDown=this.initDrag.bindAsEventListener(this); +Event.observe(this.handle,"mousedown",this.eventMouseDown); +Draggables.register(this); +},draw:function(_2){ +var _3=Position.cumulativeOffset(this.element); +var d=this.currentDelta(); +_3[0]-=d[0]; +_3[1]-=d[1]; +var p=[0,1].map(function(i){ +return (_2[i]-_3[i]-this.offset[i]); +}.bind(this)); +this.options.drawMethod(p); +}}); +var Cropper={}; +Cropper.Img=Class.create(); +Cropper.Img.prototype={initialize:function(_7,_8){ +this.options=Object.extend({ratioDim:{x:0,y:0},minWidth:0,minHeight:0,displayOnInit:false,onEndCrop:Prototype.emptyFunction,captureKeys:true,onloadCoords:null,maxWidth:0,maxHeight:0},_8||{}); +this.img=$(_7); +this.clickCoords={x:0,y:0}; +this.dragging=false; +this.resizing=false; +this.isWebKit=/Konqueror|Safari|KHTML/.test(navigator.userAgent); +this.isIE=/MSIE/.test(navigator.userAgent); +this.isOpera8=/Opera\s[1-8]/.test(navigator.userAgent); +this.ratioX=0; +this.ratioY=0; +this.attached=false; +this.fixedWidth=(this.options.maxWidth>0&&(this.options.minWidth>=this.options.maxWidth)); +this.fixedHeight=(this.options.maxHeight>0&&(this.options.minHeight>=this.options.maxHeight)); +if(typeof this.img=="undefined"){ +return; +} +$A(document.getElementsByTagName("script")).each(function(s){ +if(s.src.match(/cropper\.js/)){ +var _a=s.src.replace(/cropper\.js(.*)?/,""); +var _b=document.createElement("link"); +_b.rel="stylesheet"; +_b.type="text/css"; +_b.href=_a+"cropper.css"; +_b.media="screen"; +document.getElementsByTagName("head")[0].appendChild(_b); +} +}); +if(this.options.ratioDim.x>0&&this.options.ratioDim.y>0){ +var _c=this.getGCD(this.options.ratioDim.x,this.options.ratioDim.y); +this.ratioX=this.options.ratioDim.x/_c; +this.ratioY=this.options.ratioDim.y/_c; +} +this.subInitialize(); +if(this.img.complete||this.isWebKit){ +this.onLoad(); +}else{ +Event.observe(this.img,"load",this.onLoad.bindAsEventListener(this)); +} +},getGCD:function(a,b){ +if(b==0){ +return a; +} +return this.getGCD(b,a%b); +},onLoad:function(){ +var _f="imgCrop_"; +var _10=this.img.parentNode; +var _11=""; +if(this.isOpera8){ +_11=" opera8"; +} +this.imgWrap=Builder.node("div",{"class":_f+"wrap"+_11}); +this.north=Builder.node("div",{"class":_f+"overlay "+_f+"north"},[Builder.node("span")]); +this.east=Builder.node("div",{"class":_f+"overlay "+_f+"east"},[Builder.node("span")]); +this.south=Builder.node("div",{"class":_f+"overlay "+_f+"south"},[Builder.node("span")]); +this.west=Builder.node("div",{"class":_f+"overlay "+_f+"west"},[Builder.node("span")]); +var _12=[this.north,this.east,this.south,this.west]; +this.dragArea=Builder.node("div",{"class":_f+"dragArea"},_12); +this.handleN=Builder.node("div",{"class":_f+"handle "+_f+"handleN"}); +this.handleNE=Builder.node("div",{"class":_f+"handle "+_f+"handleNE"}); +this.handleE=Builder.node("div",{"class":_f+"handle "+_f+"handleE"}); +this.handleSE=Builder.node("div",{"class":_f+"handle "+_f+"handleSE"}); +this.handleS=Builder.node("div",{"class":_f+"handle "+_f+"handleS"}); +this.handleSW=Builder.node("div",{"class":_f+"handle "+_f+"handleSW"}); +this.handleW=Builder.node("div",{"class":_f+"handle "+_f+"handleW"}); +this.handleNW=Builder.node("div",{"class":_f+"handle "+_f+"handleNW"}); +this.selArea=Builder.node("div",{"class":_f+"selArea"},[Builder.node("div",{"class":_f+"marqueeHoriz "+_f+"marqueeNorth"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeVert "+_f+"marqueeEast"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeHoriz "+_f+"marqueeSouth"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeVert "+_f+"marqueeWest"},[Builder.node("span")]),this.handleN,this.handleNE,this.handleE,this.handleSE,this.handleS,this.handleSW,this.handleW,this.handleNW,Builder.node("div",{"class":_f+"clickArea"})]); +this.imgWrap.appendChild(this.img); +this.imgWrap.appendChild(this.dragArea); +this.dragArea.appendChild(this.selArea); +this.dragArea.appendChild(Builder.node("div",{"class":_f+"clickArea"})); +_10.appendChild(this.imgWrap); +this.startDragBind=this.startDrag.bindAsEventListener(this); +Event.observe(this.dragArea,"mousedown",this.startDragBind); +this.onDragBind=this.onDrag.bindAsEventListener(this); +Event.observe(document,"mousemove",this.onDragBind); +this.endCropBind=this.endCrop.bindAsEventListener(this); +Event.observe(document,"mouseup",this.endCropBind); +this.resizeBind=this.startResize.bindAsEventListener(this); +this.handles=[this.handleN,this.handleNE,this.handleE,this.handleSE,this.handleS,this.handleSW,this.handleW,this.handleNW]; +this.registerHandles(true); +if(this.options.captureKeys){ +this.keysBind=this.handleKeys.bindAsEventListener(this); +Event.observe(document,"keypress",this.keysBind); +} +new CropDraggable(this.selArea,{drawMethod:this.moveArea.bindAsEventListener(this)}); +this.setParams(); +},registerHandles:function(_13){ +for(var i=0;i0&&this.options.ratioDim.y>0){ +_1a.x1=Math.ceil((this.imgW-this.options.ratioDim.x)/2); +_1a.y1=Math.ceil((this.imgH-this.options.ratioDim.y)/2); +_1a.x2=_1a.x1+this.options.ratioDim.x; +_1a.y2=_1a.y1+this.options.ratioDim.y; +_1b=true; +} +} +this.setAreaCoords(_1a,false,false,1); +if(this.options.displayOnInit&&_1b){ +this.selArea.show(); +this.drawArea(); +this.endCrop(); +} +this.attached=true; +},remove:function(){ +if(this.attached){ +this.attached=false; +this.imgWrap.parentNode.insertBefore(this.img,this.imgWrap); +this.imgWrap.parentNode.removeChild(this.imgWrap); +Event.stopObserving(this.dragArea,"mousedown",this.startDragBind); +Event.stopObserving(document,"mousemove",this.onDragBind); +Event.stopObserving(document,"mouseup",this.endCropBind); +this.registerHandles(false); +if(this.options.captureKeys){ +Event.stopObserving(document,"keypress",this.keysBind); +} +} +},reset:function(){ +if(!this.attached){ +this.onLoad(); +}else{ +this.setParams(); +} +this.endCrop(); +},handleKeys:function(e){ +var dir={x:0,y:0}; +if(!this.dragging){ +switch(e.keyCode){ +case (37): +dir.x=-1; +break; +case (38): +dir.y=-1; +break; +case (39): +dir.x=1; +break; +case (40): +dir.y=1; +break; +} +if(dir.x!=0||dir.y!=0){ +if(e.shiftKey){ +dir.x*=10; +dir.y*=10; +} +this.moveArea([this.areaCoords.x1+dir.x,this.areaCoords.y1+dir.y]); +Event.stop(e); +} +} +},calcW:function(){ +return (this.areaCoords.x2-this.areaCoords.x1); +},calcH:function(){ +return (this.areaCoords.y2-this.areaCoords.y1); +},moveArea:function(_1e){ +this.setAreaCoords({x1:_1e[0],y1:_1e[1],x2:_1e[0]+this.calcW(),y2:_1e[1]+this.calcH()},true,false); +this.drawArea(); +},cloneCoords:function(_1f){ +return {x1:_1f.x1,y1:_1f.y1,x2:_1f.x2,y2:_1f.y2}; +},setAreaCoords:function(_20,_21,_22,_23,_24){ +if(_21){ +var _25=_20.x2-_20.x1; +var _26=_20.y2-_20.y1; +if(_20.x1<0){ +_20.x1=0; +_20.x2=_25; +} +if(_20.y1<0){ +_20.y1=0; +_20.y2=_26; +} +if(_20.x2>this.imgW){ +_20.x2=this.imgW; +_20.x1=this.imgW-_25; +} +if(_20.y2>this.imgH){ +_20.y2=this.imgH; +_20.y1=this.imgH-_26; +} +}else{ +if(_20.x1<0){ +_20.x1=0; +} +if(_20.y1<0){ +_20.y1=0; +} +if(_20.x2>this.imgW){ +_20.x2=this.imgW; +} +if(_20.y2>this.imgH){ +_20.y2=this.imgH; +} +if(_23!=null){ +if(this.ratioX>0){ +this.applyRatio(_20,{x:this.ratioX,y:this.ratioY},_23,_24); +}else{ +if(_22){ +this.applyRatio(_20,{x:1,y:1},_23,_24); +} +} +var _27=[this.options.minWidth,this.options.minHeight]; +var _28=[this.options.maxWidth,this.options.maxHeight]; +if(_27[0]>0||_27[1]>0||_28[0]>0||_28[1]>0){ +var _29={a1:_20.x1,a2:_20.x2}; +var _2a={a1:_20.y1,a2:_20.y2}; +var _2b={min:0,max:this.imgW}; +var _2c={min:0,max:this.imgH}; +if((_27[0]!=0||_27[1]!=0)&&_22){ +if(_27[0]>0){ +_27[1]=_27[0]; +}else{ +if(_27[1]>0){ +_27[0]=_27[1]; +} +} +} +if((_28[0]!=0||_28[0]!=0)&&_22){ +if(_28[0]>0&&_28[0]<=_28[1]){ +_28[1]=_28[0]; +}else{ +if(_28[1]>0&&_28[1]<=_28[0]){ +_28[0]=_28[1]; +} +} +} +if(_27[0]>0){ +this.applyDimRestriction(_29,_27[0],_23.x,_2b,"min"); +} +if(_27[1]>1){ +this.applyDimRestriction(_2a,_27[1],_23.y,_2c,"min"); +} +if(_28[0]>0){ +this.applyDimRestriction(_29,_28[0],_23.x,_2b,"max"); +} +if(_28[1]>1){ +this.applyDimRestriction(_2a,_28[1],_23.y,_2c,"max"); +} +_20={x1:_29.a1,y1:_2a.a1,x2:_29.a2,y2:_2a.a2}; +} +} +} +this.areaCoords=_20; +},applyDimRestriction:function(_2d,val,_2f,_30,_31){ +var _32; +if(_31=="min"){ +_32=((_2d.a2-_2d.a1)val); +} +if(_32){ +if(_2f==1){ +_2d.a2=_2d.a1+val; +}else{ +_2d.a1=_2d.a2-val; +} +if(_2d.a1<_30.min){ +_2d.a1=_30.min; +_2d.a2=val; +}else{ +if(_2d.a2>_30.max){ +_2d.a1=_30.max-val; +_2d.a2=_30.max; +} +} +} +},applyRatio:function(_33,_34,_35,_36){ +var _37; +if(_36=="N"||_36=="S"){ +_37=this.applyRatioToAxis({a1:_33.y1,b1:_33.x1,a2:_33.y2,b2:_33.x2},{a:_34.y,b:_34.x},{a:_35.y,b:_35.x},{min:0,max:this.imgW}); +_33.x1=_37.b1; +_33.y1=_37.a1; +_33.x2=_37.b2; +_33.y2=_37.a2; +}else{ +_37=this.applyRatioToAxis({a1:_33.x1,b1:_33.y1,a2:_33.x2,b2:_33.y2},{a:_34.x,b:_34.y},{a:_35.x,b:_35.y},{min:0,max:this.imgH}); +_33.x1=_37.a1; +_33.y1=_37.b1; +_33.x2=_37.a2; +_33.y2=_37.b2; +} +},applyRatioToAxis:function(_38,_39,_3a,_3b){ +var _3c=Object.extend(_38,{}); +var _3d=_3c.a2-_3c.a1; +var _3e=Math.floor(_3d*_39.b/_39.a); +var _3f; +var _40; +var _41=null; +if(_3a.b==1){ +_3f=_3c.b1+_3e; +if(_3f>_3b.max){ +_3f=_3b.max; +_41=_3f-_3c.b1; +} +_3c.b2=_3f; +}else{ +_3f=_3c.b2-_3e; +if(_3f<_3b.min){ +_3f=_3b.min; +_41=_3f+_3c.b2; +} +_3c.b1=_3f; +} +if(_41!=null){ +_40=Math.floor(_41*_39.a/_39.b); +if(_3a.a==1){ +_3c.a2=_3c.a1+_40; +}else{ +_3c.a1=_3c.a1=_3c.a2-_40; +} +} +return _3c; +},drawArea:function(){ +var _42=this.calcW(); +var _43=this.calcH(); +var px="px"; +var _45=[this.areaCoords.x1+px,this.areaCoords.y1+px,_42+px,_43+px,this.areaCoords.x2+px,this.areaCoords.y2+px,(this.img.width-this.areaCoords.x2)+px,(this.img.height-this.areaCoords.y2)+px]; +var _46=this.selArea.style; +_46.left=_45[0]; +_46.top=_45[1]; +_46.width=_45[2]; +_46.height=_45[3]; +var _47=Math.ceil((_42-6)/2)+px; +var _48=Math.ceil((_43-6)/2)+px; +this.handleN.style.left=_47; +this.handleE.style.top=_48; +this.handleS.style.left=_47; +this.handleW.style.top=_48; +this.north.style.height=_45[1]; +var _49=this.east.style; +_49.top=_45[1]; +_49.height=_45[3]; +_49.left=_45[4]; +_49.width=_45[6]; +var _4a=this.south.style; +_4a.top=_45[5]; +_4a.height=_45[7]; +var _4b=this.west.style; +_4b.top=_45[1]; +_4b.height=_45[3]; +_4b.width=_45[0]; +this.subDrawArea(); +this.forceReRender(); +},forceReRender:function(){ +if(this.isIE||this.isWebKit){ +var n=document.createTextNode(" "); +var d,el,fixEL,i; +if(this.isIE){ +fixEl=this.selArea; +}else{ +if(this.isWebKit){ +fixEl=document.getElementsByClassName("imgCrop_marqueeSouth",this.imgWrap)[0]; +d=Builder.node("div",""); +d.style.visibility="hidden"; +var _4e=["SE","S","SW"]; +for(i=0;i<_4e.length;i++){ +el=document.getElementsByClassName("imgCrop_handle"+_4e[i],this.selArea)[0]; +if(el.childNodes.length){ +el.removeChild(el.childNodes[0]); +} +el.appendChild(d); +} +} +} +fixEl.appendChild(n); +fixEl.removeChild(n); +} +},startResize:function(e){ +this.startCoords=this.cloneCoords(this.areaCoords); +this.resizing=true; +this.resizeHandle=Event.element(e).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/,""); +Event.stop(e); +},startDrag:function(e){ +this.selArea.show(); +this.clickCoords=this.getCurPos(e); +this.setAreaCoords({x1:this.clickCoords.x,y1:this.clickCoords.y,x2:this.clickCoords.x,y2:this.clickCoords.y},false,false,null); +this.dragging=true; +this.onDrag(e); +Event.stop(e); +},getCurPos:function(e){ +var el=this.imgWrap,wrapOffsets=Position.cumulativeOffset(el); +while(el.nodeName!="BODY"){ +wrapOffsets[1]-=el.scrollTop||0; +wrapOffsets[0]-=el.scrollLeft||0; +el=el.parentNode; +} +return curPos={x:Event.pointerX(e)-wrapOffsets[0],y:Event.pointerY(e)-wrapOffsets[1]}; +},onDrag:function(e){ +if(this.dragging||this.resizing){ +var _54=null; +var _55=this.getCurPos(e); +var _56=this.cloneCoords(this.areaCoords); +var _57={x:1,y:1}; +if(this.dragging){ +if(_55.x_59){ +_5c.reverse(); +} +_5a[_5b+"1"]=_5c[0]; +_5a[_5b+"2"]=_5c[1]; +},endCrop:function(){ +this.dragging=false; +this.resizing=false; +this.options.onEndCrop(this.areaCoords,{width:this.calcW(),height:this.calcH()}); +},subInitialize:function(){ +},subDrawArea:function(){ +}}; +Cropper.ImgWithPreview=Class.create(); +Object.extend(Object.extend(Cropper.ImgWithPreview.prototype,Cropper.Img.prototype),{subInitialize:function(){ +this.hasPreviewImg=false; +if(typeof (this.options.previewWrap)!="undefined"&&this.options.minWidth>0&&this.options.minHeight>0){ +this.previewWrap=$(this.options.previewWrap); +this.previewImg=this.img.cloneNode(false); +this.previewImg.id="imgCrop_"+this.previewImg.id; +this.options.displayOnInit=true; +this.hasPreviewImg=true; +this.previewWrap.addClassName("imgCrop_previewWrap"); +this.previewWrap.setStyle({width:this.options.minWidth+"px",height:this.options.minHeight+"px"}); +this.previewWrap.appendChild(this.previewImg); +} +},subDrawArea:function(){ +if(this.hasPreviewImg){ +var _5d=this.calcW(); +var _5e=this.calcH(); +var _5f={x:this.imgW/_5d,y:this.imgH/_5e}; +var _60={x:_5d/this.options.minWidth,y:_5e/this.options.minHeight}; +var _61={w:Math.ceil(this.options.minWidth*_5f.x)+"px",h:Math.ceil(this.options.minHeight*_5f.y)+"px",x:"-"+Math.ceil(this.areaCoords.x1/_60.x)+"px",y:"-"+Math.ceil(this.areaCoords.y1/_60.y)+"px"}; +var _62=this.previewImg.style; +_62.width=_61.w; +_62.height=_61.h; +_62.left=_61.x; +_62.top=_61.y; +} +}}); + diff --git a/cropper/cropper.uncompressed.js b/cropper/cropper.uncompressed.js new file mode 100644 index 0000000..6618554 --- /dev/null +++ b/cropper/cropper.uncompressed.js @@ -0,0 +1,1331 @@ +/** + * Image Cropper (v. 1.2.0 - 2006-10-30 ) + * Copyright (c) 2006 David Spurr (http://www.defusion.org.uk/) + * + * The image cropper provides a way to draw a crop area on an image and capture + * the coordinates of the drawn crop area. + * + * Features include: + * - Based on Prototype and Scriptaculous + * - Image editing package styling, the crop area functions and looks + * like those found in popular image editing software + * - Dynamic inclusion of required styles + * - Drag to draw areas + * - Shift drag to draw/resize areas as squares + * - Selection area can be moved + * - Seleciton area can be resized using resize handles + * - Allows dimension ratio limited crop areas + * - Allows minimum dimension crop areas + * - Allows maximum dimesion crop areas + * - If both min & max dimension options set to the same value for a single axis,then the cropper will not + * display the resize handles as appropriate (when min & max dimensions are passed for both axes this + * results in a 'fixed size' crop area) + * - Allows dynamic preview of resultant crop ( if minimum width & height are provided ), this is + * implemented as a subclass so can be excluded when not required + * - Movement of selection area by arrow keys ( shift + arrow key will move selection area by + * 10 pixels ) + * - All operations stay within bounds of image + * - All functionality & display compatible with most popular browsers supported by Prototype: + * PC: IE 7, 6 & 5.5, Firefox 1.5, Opera 8.5 (see known issues) & 9.0b + * MAC: Camino 1.0, Firefox 1.5, Safari 2.0 + * + * Requires: + * - Prototype v. 1.5.0_rc0 > (as packaged with Scriptaculous 1.6.1) + * - Scriptaculous v. 1.6.1 > modules: builder, dragdrop + * + * Known issues: + * - Safari animated gifs, only one of each will animate, this seems to be a known Safari issue + * + * - After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height + * appears as the last height until the user drags, this appears to be the related to the error + * that the forceReRender() method fixes for IE 6, i.e. IE 5.5 is not redrawing the box properly. + * + * - Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, these + * could be fixed by using PNGs with transparency if Opera 8.5 support is high priority for you + * + * - Marching ants keep reloading in IE <6 (not tested in IE7), it is a known issue in IE and I have + * found no viable workarounds that can be included in the release. If this really is an issue for you + * either try this post: http://mir.aculo.us/articles/2005/08/28/internet-explorer-and-ajax-image-caching-woes + * or uncomment the 'FIX MARCHING ANTS IN IE' rules in the CSS file + * + * - Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will + * cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended. + * + * - overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera (maybe Mac browsers too) + * I'm not sure why yet. + * + * Usage: + * See Cropper.Img & Cropper.ImgWithPreview for usage details + * + * Changelog: + * v1.2.0 - 2006-10-30 + * + Added id to the preview image element using 'imgCrop_[originalImageID]' + * * #00001 - Fixed bug: Doesn't account for scroll offsets + * * #00009 - Fixed bug: Placing the cropper inside differently positioned elements causes incorrect co-ordinates and display + * * #00013 - Fixed bug: I-bar cursor appears on drag plane + * * #00014 - Fixed bug: If ID for image tag is not found in document script throws error + * * Fixed bug with drag start co-ordinates if wrapper element has moved in browser (e.g. dragged to a new position) + * * Fixed bug with drag start co-ordinates if image contained in a wrapper with scrolling - this may be buggy if image + * has other ancestors with scrolling applied (except the body) + * * #00015 - Fixed bug: When cropper removed and then reapplied onEndCrop callback gets called multiple times, solution suggestion from Bill Smith + * * Various speed increases & code cleanup which meant improved performance in Mac - which allowed removal of different overlay methods for + * IE and all other browsers, which led to a fix for: + * * #00010 - Fixed bug: Select area doesn't adhere to image size when image resized using img attributes + * - #00006 - Removed default behaviour of automatically setting a ratio when both min width & height passed, the ratioDimensions must be passed in + * + #00005 - Added ability to set maximum crop dimensions, if both min & max set as the same value then we'll get a fixed cropper size on the axes as appropriate + * and the resize handles will not be displayed as appropriate + * * Switched keydown for keypress for moving select area with cursor keys (makes for nicer action) - doesn't appear to work in Safari + * + * v1.1.3 - 2006-08-21 + * * Fixed wrong cursor on western handle in CSS + * + #00008 & #00003 - Added feature: Allow to set dimensions & position for cropper on load + * * #00002 - Fixed bug: Pressing 'remove cropper' twice removes image in IE + * + * v1.1.2 - 2006-06-09 + * * Fixed bugs with ratios when GCD is low (patch submitted by Andy Skelton) + * + * v1.1.1 - 2006-06-03 + * * Fixed bug with rendering issues fix in IE 5.5 + * * Fixed bug with endCrop callback issues once cropper had been removed & reset in IE + * + * v1.1.0 - 2006-06-02 + * * Fixed bug with IE constantly trying to reload select area background image + * * Applied more robust fix to Safari & IE rendering issues + * + Added method to reset parameters - useful for when dynamically changing img cropper attached to + * + Added method to remove cropper from image + * + * v1.0.0 - 2006-05-18 + * + Initial verison + * + * + * Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/) + * 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 David Spurr 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. + * + * http://www.opensource.org/licenses/bsd-license.php + * + * See scriptaculous.js for full scriptaculous licence + */ + +/** + * Extend the Draggable class to allow us to pass the rendering + * down to the Cropper object. + */ +var CropDraggable = Class.create(); + +Object.extend( Object.extend( CropDraggable.prototype, Draggable.prototype), { + + initialize: function(element) { + this.options = Object.extend( + { + /** + * The draw method to defer drawing to + */ + drawMethod: function() {} + }, + arguments[1] || {} + ); + + this.element = $(element); + + this.handle = this.element; + + this.delta = this.currentDelta(); + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + /** + * Defers the drawing of the draggable to the supplied method + */ + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; + pos[1] -= d[1]; + + var p = [0,1].map(function(i) { + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + this.options.drawMethod( p ); + } + +}); + + +/** + * The Cropper object, this will attach itself to the provided image by wrapping it with + * the generated xHTML structure required by the cropper. + * + * Usage: + * @param obj Image element to attach to + * @param obj Optional options: + * - ratioDim obj + * The pixel dimensions to apply as a restrictive ratio, with properties x & y + * + * - minWidth int + * The minimum width for the select area in pixels + * + * - minHeight int + * The mimimum height for the select area in pixels + * + * - maxWidth int + * The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed) + * + * - maxHeight int + * The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed) + * + * - displayOnInit int + * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio + * + * - onEndCrop func + * The callback function to provide the crop details to on end of a crop (see below) + * + * - captureKeys boolean + * Whether to capture the keys for moving the select area, as these can cause some problems at the moment + * + * - onloadCoords obj + * A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload + * + *---------------------------------------------- + * + * The callback function provided via the onEndCrop option should accept the following parameters: + * - coords obj + * The coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area + * + * - dimensions obj + * The dimensions object with properites width & height; for the dimensions of the select area + * + * + * Example: + * function onEndCrop( coords, dimensions ) { + * $( 'x1' ).value = coords.x1; + * $( 'y1' ).value = coords.y1; + * $( 'x2' ).value = coords.x2; + * $( 'y2' ).value = coords.y2; + * $( 'width' ).value = dimensions.width; + * $( 'height' ).value = dimensions.height; + * } + * + */ +var Cropper = {}; +Cropper.Img = Class.create(); +Cropper.Img.prototype = { + + /** + * Initialises the class + * + * @access public + * @param obj Image element to attach to + * @param obj Options + * @return void + */ + initialize: function(element, options) { + this.options = Object.extend( + { + /** + * @var obj + * The pixel dimensions to apply as a restrictive ratio + */ + ratioDim: { x: 0, y: 0 }, + /** + * @var int + * The minimum pixel width, also used as restrictive ratio if min height passed too + */ + minWidth: 0, + /** + * @var int + * The minimum pixel height, also used as restrictive ratio if min width passed too + */ + minHeight: 0, + /** + * @var boolean + * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio + */ + displayOnInit: false, + /** + * @var function + * The call back function to pass the final values to + */ + onEndCrop: Prototype.emptyFunction, + /** + * @var boolean + * Whether to capture key presses or not + */ + captureKeys: true, + /** + * @var obj Coordinate object x1, y1, x2, y2 + * The coordinates to optionally display the select area at onload + */ + onloadCoords: null, + /** + * @var int + * The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed) + */ + maxWidth: 0, + /** + * @var int + * The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed) + */ + maxHeight: 0 + }, + options || {} + ); + /** + * @var obj + * The img node to attach to + */ + this.img = $( element ); + /** + * @var obj + * The x & y coordinates of the click point + */ + this.clickCoords = { x: 0, y: 0 }; + /** + * @var boolean + * Whether the user is dragging + */ + this.dragging = false; + /** + * @var boolean + * Whether the user is resizing + */ + this.resizing = false; + /** + * @var boolean + * Whether the user is on a webKit browser + */ + this.isWebKit = /Konqueror|Safari|KHTML/.test( navigator.userAgent ); + /** + * @var boolean + * Whether the user is on IE + */ + this.isIE = /MSIE/.test( navigator.userAgent ); + /** + * @var boolean + * Whether the user is on Opera below version 9 + */ + this.isOpera8 = /Opera\s[1-8]/.test( navigator.userAgent ); + /** + * @var int + * The x ratio + */ + this.ratioX = 0; + /** + * @var int + * The y ratio + */ + this.ratioY = 0; + /** + * @var boolean + * Whether we've attached sucessfully + */ + this.attached = false; + /** + * @var boolean + * Whether we've got a fixed width (if minWidth EQ or GT maxWidth then we have a fixed width + * in the case of minWidth > maxWidth maxWidth wins as the fixed width) + */ + this.fixedWidth = ( this.options.maxWidth > 0 && ( this.options.minWidth >= this.options.maxWidth ) ); + /** + * @var boolean + * Whether we've got a fixed height (if minHeight EQ or GT maxHeight then we have a fixed height + * in the case of minHeight > maxHeight maxHeight wins as the fixed height) + */ + this.fixedHeight = ( this.options.maxHeight > 0 && ( this.options.minHeight >= this.options.maxHeight ) ); + + // quit if the image element doesn't exist + if( typeof this.img == 'undefined' ) return; + + // include the stylesheet + $A( document.getElementsByTagName( 'script' ) ).each( + function(s) { + if( s.src.match( /cropper\.js/ ) ) { + var path = s.src.replace( /cropper\.js(.*)?/, '' ); + // ''; + var style = document.createElement( 'link' ); + style.rel = 'stylesheet'; + style.type = 'text/css'; + style.href = path + 'cropper.css'; + style.media = 'screen'; + document.getElementsByTagName( 'head' )[0].appendChild( style ); + } + } + ); + + // calculate the ratio when neccessary + if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) { + var gcd = this.getGCD( this.options.ratioDim.x, this.options.ratioDim.y ); + this.ratioX = this.options.ratioDim.x / gcd; + this.ratioY = this.options.ratioDim.y / gcd; + // dump( 'RATIO : ' + this.ratioX + ':' + this.ratioY + '\n' ); + } + + // initialise sub classes + this.subInitialize(); + + // only load the event observers etc. once the image is loaded + // this is done after the subInitialize() call just in case the sub class does anything + // that will affect the result of the call to onLoad() + if( this.img.complete || this.isWebKit ) this.onLoad(); // for some reason Safari seems to support img.complete but returns 'undefined' on the this.img object + else Event.observe( this.img, 'load', this.onLoad.bindAsEventListener( this) ); + }, + + /** + * The Euclidean algorithm used to find the greatest common divisor + * + * @acces private + * @param int Value 1 + * @param int Value 2 + * @return int + */ + getGCD : function( a , b ) { + if( b == 0 ) return a; + return this.getGCD(b, a % b ); + }, + + /** + * Attaches the cropper to the image once it has loaded + * + * @access private + * @return void + */ + onLoad: function( ) { + /* + * Build the container and all related elements, will result in the following + * + *
+ * + *
+ * + *
+ *
+ *
+ *
+ *
+ * + * + *
+ *
+ *
+ *
+ * + *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */ + var cNamePrefix = 'imgCrop_'; + + // get the point to insert the container + var insertPoint = this.img.parentNode; + + // apply an extra class to the wrapper to fix Opera below version 9 + var fixOperaClass = ''; + if( this.isOpera8 ) fixOperaClass = ' opera8'; + this.imgWrap = Builder.node( 'div', { 'class': cNamePrefix + 'wrap' + fixOperaClass } ); + + this.north = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'north' }, [Builder.node( 'span' )] ); + this.east = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'east' } , [Builder.node( 'span' )] ); + this.south = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'south' }, [Builder.node( 'span' )] ); + this.west = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'west' } , [Builder.node( 'span' )] ); + + var overlays = [ this.north, this.east, this.south, this.west ]; + + this.dragArea = Builder.node( 'div', { 'class': cNamePrefix + 'dragArea' }, overlays ); + + this.handleN = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleN' } ); + this.handleNE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNE' } ); + this.handleE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleE' } ); + this.handleSE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSE' } ); + this.handleS = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleS' } ); + this.handleSW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSW' } ); + this.handleW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleW' } ); + this.handleNW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNW' } ); + + this.selArea = Builder.node( 'div', { 'class': cNamePrefix + 'selArea' }, + [ + Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeNorth' }, [Builder.node( 'span' )] ), + Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeEast' } , [Builder.node( 'span' )] ), + Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeSouth' }, [Builder.node( 'span' )] ), + Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeWest' } , [Builder.node( 'span' )] ), + this.handleN, + this.handleNE, + this.handleE, + this.handleSE, + this.handleS, + this.handleSW, + this.handleW, + this.handleNW, + Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } ) + ] + ); + + this.imgWrap.appendChild( this.img ); + this.imgWrap.appendChild( this.dragArea ); + this.dragArea.appendChild( this.selArea ); + this.dragArea.appendChild( Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } ) ); + + insertPoint.appendChild( this.imgWrap ); + + // add event observers + this.startDragBind = this.startDrag.bindAsEventListener( this ); + Event.observe( this.dragArea, 'mousedown', this.startDragBind ); + + this.onDragBind = this.onDrag.bindAsEventListener( this ); + Event.observe( document, 'mousemove', this.onDragBind ); + + this.endCropBind = this.endCrop.bindAsEventListener( this ); + Event.observe( document, 'mouseup', this.endCropBind ); + + this.resizeBind = this.startResize.bindAsEventListener( this ); + this.handles = [ this.handleN, this.handleNE, this.handleE, this.handleSE, this.handleS, this.handleSW, this.handleW, this.handleNW ]; + this.registerHandles( true ); + + if( this.options.captureKeys ) { + this.keysBind = this.handleKeys.bindAsEventListener( this ); + Event.observe( document, 'keypress', this.keysBind ); + } + + // attach the dragable to the select area + new CropDraggable( this.selArea, { drawMethod: this.moveArea.bindAsEventListener( this ) } ); + + this.setParams(); + }, + + /** + * Manages adding or removing the handle event handler and hiding or displaying them as appropriate + * + * @access private + * @param boolean registration true = add, false = remove + * @return void + */ + registerHandles: function( registration ) { + for( var i = 0; i < this.handles.length; i++ ) { + var handle = $( this.handles[i] ); + + if( registration ) { + var hideHandle = false; // whether to hide the handle + + // disable handles asappropriate if we've got fixed dimensions + // if both dimensions are fixed we don't need to do much + if( this.fixedWidth && this.fixedHeight ) hideHandle = true; + else if( this.fixedWidth || this.fixedHeight ) { + // if one of the dimensions is fixed then just hide those handles + var isCornerHandle = handle.className.match( /([S|N][E|W])$/ ) + var isWidthHandle = handle.className.match( /(E|W)$/ ); + var isHeightHandle = handle.className.match( /(N|S)$/ ); + if( isCornerHandle ) hideHandle = true; + else if( this.fixedWidth && isWidthHandle ) hideHandle = true; + else if( this.fixedHeight && isHeightHandle ) hideHandle = true; + } + if( hideHandle ) handle.hide(); + else Event.observe( handle, 'mousedown', this.resizeBind ); + } else { + handle.show(); + Event.stopObserving( handle, 'mousedown', this.resizeBind ); + } + } + }, + + /** + * Sets up all the cropper parameters, this can be used to reset the cropper when dynamically + * changing the images + * + * @access private + * @return void + */ + setParams: function() { + /** + * @var int + * The image width + */ + this.imgW = this.img.width; + /** + * @var int + * The image height + */ + this.imgH = this.img.height; + + $( this.north ).setStyle( { height: 0 } ); + $( this.east ).setStyle( { width: 0, height: 0 } ); + $( this.south ).setStyle( { height: 0 } ); + $( this.west ).setStyle( { width: 0, height: 0 } ); + + // resize the container to fit the image + $( this.imgWrap ).setStyle( { 'width': this.imgW + 'px', 'height': this.imgH + 'px' } ); + + // hide the select area + $( this.selArea ).hide(); + + // setup the starting position of the select area + var startCoords = { x1: 0, y1: 0, x2: 0, y2: 0 }; + var validCoordsSet = false; + + // display the select area + if( this.options.onloadCoords != null ) { + // if we've being given some coordinates to + startCoords = this.cloneCoords( this.options.onloadCoords ); + validCoordsSet = true; + } else if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) { + // if there is a ratio limit applied and the then set it to initial ratio + startCoords.x1 = Math.ceil( ( this.imgW - this.options.ratioDim.x ) / 2 ); + startCoords.y1 = Math.ceil( ( this.imgH - this.options.ratioDim.y ) / 2 ); + startCoords.x2 = startCoords.x1 + this.options.ratioDim.x; + startCoords.y2 = startCoords.y1 + this.options.ratioDim.y; + validCoordsSet = true; + } + + this.setAreaCoords( startCoords, false, false, 1 ); + + if( this.options.displayOnInit && validCoordsSet ) { + this.selArea.show(); + this.drawArea(); + this.endCrop(); + } + + this.attached = true; + }, + + /** + * Removes the cropper + * + * @access public + * @return void + */ + remove: function() { + if( this.attached ) { + this.attached = false; + + // remove the elements we inserted + this.imgWrap.parentNode.insertBefore( this.img, this.imgWrap ); + this.imgWrap.parentNode.removeChild( this.imgWrap ); + + // remove the event observers + Event.stopObserving( this.dragArea, 'mousedown', this.startDragBind ); + Event.stopObserving( document, 'mousemove', this.onDragBind ); + Event.stopObserving( document, 'mouseup', this.endCropBind ); + this.registerHandles( false ); + if( this.options.captureKeys ) Event.stopObserving( document, 'keypress', this.keysBind ); + } + }, + + /** + * Resets the cropper, can be used either after being removed or any time you wish + * + * @access public + * @return void + */ + reset: function() { + if( !this.attached ) this.onLoad(); + else this.setParams(); + this.endCrop(); + }, + + /** + * Handles the key functionality, currently just using arrow keys to move, if the user + * presses shift then the area will move by 10 pixels + */ + handleKeys: function( e ) { + var dir = { x: 0, y: 0 }; // direction to move it in & the amount in pixels + if( !this.dragging ) { + + // catch the arrow keys + switch( e.keyCode ) { + case( 37 ) : // left + dir.x = -1; + break; + case( 38 ) : // up + dir.y = -1; + break; + case( 39 ) : // right + dir.x = 1; + break + case( 40 ) : // down + dir.y = 1; + break; + } + + if( dir.x != 0 || dir.y != 0 ) { + // if shift is pressed then move by 10 pixels + if( e.shiftKey ) { + dir.x *= 10; + dir.y *= 10; + } + + this.moveArea( [ this.areaCoords.x1 + dir.x, this.areaCoords.y1 + dir.y ] ); + Event.stop( e ); + } + } + }, + + /** + * Calculates the width from the areaCoords + * + * @access private + * @return int + */ + calcW: function() { + return (this.areaCoords.x2 - this.areaCoords.x1) + }, + + /** + * Calculates the height from the areaCoords + * + * @access private + * @return int + */ + calcH: function() { + return (this.areaCoords.y2 - this.areaCoords.y1) + }, + + /** + * Moves the select area to the supplied point (assumes the point is x1 & y1 of the select area) + * + * @access public + * @param array Point for x1 & y1 to move select area to + * @return void + */ + moveArea: function( point ) { + // dump( 'moveArea : ' + point[0] + ',' + point[1] + ',' + ( point[0] + ( this.areaCoords.x2 - this.areaCoords.x1 ) ) + ',' + ( point[1] + ( this.areaCoords.y2 - this.areaCoords.y1 ) ) + '\n' ); + this.setAreaCoords( + { + x1: point[0], + y1: point[1], + x2: point[0] + this.calcW(), + y2: point[1] + this.calcH() + }, + true, + false + ); + this.drawArea(); + }, + + /** + * Clones a co-ordinates object, stops problems with handling them by reference + * + * @access private + * @param obj Coordinate object x1, y1, x2, y2 + * @return obj Coordinate object x1, y1, x2, y2 + */ + cloneCoords: function( coords ) { + return { x1: coords.x1, y1: coords.y1, x2: coords.x2, y2: coords.y2 }; + }, + + /** + * Sets the select coords to those provided but ensures they don't go + * outside the bounding box + * + * @access private + * @param obj Coordinates x1, y1, x2, y2 + * @param boolean Whether this is a move + * @param boolean Whether to apply squaring + * @param obj Direction of mouse along both axis x, y ( -1 = negative, 1 = positive ) only required when moving etc. + * @param string The current resize handle || null + * @return void + */ + setAreaCoords: function( coords, moving, square, direction, resizeHandle ) { + // dump( 'setAreaCoords (in) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 ); + if( moving ) { + // if moving + var targW = coords.x2 - coords.x1; + var targH = coords.y2 - coords.y1; + + // ensure we're within the bounds + if( coords.x1 < 0 ) { + coords.x1 = 0; + coords.x2 = targW; + } + if( coords.y1 < 0 ) { + coords.y1 = 0; + coords.y2 = targH; + } + if( coords.x2 > this.imgW ) { + coords.x2 = this.imgW; + coords.x1 = this.imgW - targW; + } + if( coords.y2 > this.imgH ) { + coords.y2 = this.imgH; + coords.y1 = this.imgH - targH; + } + } else { + // ensure we're within the bounds + if( coords.x1 < 0 ) coords.x1 = 0; + if( coords.y1 < 0 ) coords.y1 = 0; + if( coords.x2 > this.imgW ) coords.x2 = this.imgW; + if( coords.y2 > this.imgH ) coords.y2 = this.imgH; + + // This is passed as null in onload + if( direction != null ) { + + // apply the ratio or squaring where appropriate + if( this.ratioX > 0 ) this.applyRatio( coords, { x: this.ratioX, y: this.ratioY }, direction, resizeHandle ); + else if( square ) this.applyRatio( coords, { x: 1, y: 1 }, direction, resizeHandle ); + + var mins = [ this.options.minWidth, this.options.minHeight ]; // minimum dimensions [x,y] + var maxs = [ this.options.maxWidth, this.options.maxHeight ]; // maximum dimensions [x,y] + + // apply dimensions where appropriate + if( mins[0] > 0 || mins[1] > 0 || maxs[0] > 0 || maxs[1] > 0) { + + var coordsTransX = { a1: coords.x1, a2: coords.x2 }; + var coordsTransY = { a1: coords.y1, a2: coords.y2 }; + var boundsX = { min: 0, max: this.imgW }; + var boundsY = { min: 0, max: this.imgH }; + + // handle squaring properly on single axis minimum dimensions + if( (mins[0] != 0 || mins[1] != 0) && square ) { + if( mins[0] > 0 ) mins[1] = mins[0]; + else if( mins[1] > 0 ) mins[0] = mins[1]; + } + + if( (maxs[0] != 0 || maxs[0] != 0) && square ) { + // if we have a max x value & it is less than the max y value then we set the y max to the max x (so we don't go over the minimum maximum of one of the axes - if that makes sense) + if( maxs[0] > 0 && maxs[0] <= maxs[1] ) maxs[1] = maxs[0]; + else if( maxs[1] > 0 && maxs[1] <= maxs[0] ) maxs[0] = maxs[1]; + } + + if( mins[0] > 0 ) this.applyDimRestriction( coordsTransX, mins[0], direction.x, boundsX, 'min' ); + if( mins[1] > 1 ) this.applyDimRestriction( coordsTransY, mins[1], direction.y, boundsY, 'min' ); + + if( maxs[0] > 0 ) this.applyDimRestriction( coordsTransX, maxs[0], direction.x, boundsX, 'max' ); + if( maxs[1] > 1 ) this.applyDimRestriction( coordsTransY, maxs[1], direction.y, boundsY, 'max' ); + + coords = { x1: coordsTransX.a1, y1: coordsTransY.a1, x2: coordsTransX.a2, y2: coordsTransY.a2 }; + } + + } + } + + // dump( 'setAreaCoords (out) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 + '\n' ); + this.areaCoords = coords; + }, + + /** + * Applies the supplied dimension restriction to the supplied coordinates along a single axis + * + * @access private + * @param obj Single axis coordinates, a1, a2 (e.g. for the x axis a1 = x1 & a2 = x2) + * @param int The restriction value + * @param int The direction ( -1 = negative, 1 = positive ) + * @param obj The bounds of the image ( for this axis ) + * @param string The dimension restriction type ( 'min' | 'max' ) + * @return void + */ + applyDimRestriction: function( coords, val, direction, bounds, type ) { + var check; + if( type == 'min' ) check = ( ( coords.a2 - coords.a1 ) < val ); + else check = ( ( coords.a2 - coords.a1 ) > val ); + if( check ) { + if( direction == 1 ) coords.a2 = coords.a1 + val; + else coords.a1 = coords.a2 - val; + + // make sure we're still in the bounds (not too pretty for the user, but needed) + if( coords.a1 < bounds.min ) { + coords.a1 = bounds.min; + coords.a2 = val; + } else if( coords.a2 > bounds.max ) { + coords.a1 = bounds.max - val; + coords.a2 = bounds.max; + } + } + }, + + /** + * Applies the supplied ratio to the supplied coordinates + * + * @access private + * @param obj Coordinates, x1, y1, x2, y2 + * @param obj Ratio, x, y + * @param obj Direction of mouse, x & y : -1 == negative 1 == positive + * @param string The current resize handle || null + * @return void + */ + applyRatio : function( coords, ratio, direction, resizeHandle ) { + // dump( 'direction.y : ' + direction.y + '\n'); + var newCoords; + if( resizeHandle == 'N' || resizeHandle == 'S' ) { + // dump( 'north south \n'); + // if moving on either the lone north & south handles apply the ratio on the y axis + newCoords = this.applyRatioToAxis( + { a1: coords.y1, b1: coords.x1, a2: coords.y2, b2: coords.x2 }, + { a: ratio.y, b: ratio.x }, + { a: direction.y, b: direction.x }, + { min: 0, max: this.imgW } + ); + coords.x1 = newCoords.b1; + coords.y1 = newCoords.a1; + coords.x2 = newCoords.b2; + coords.y2 = newCoords.a2; + } else { + // otherwise deal with it as if we're applying the ratio on the x axis + newCoords = this.applyRatioToAxis( + { a1: coords.x1, b1: coords.y1, a2: coords.x2, b2: coords.y2 }, + { a: ratio.x, b: ratio.y }, + { a: direction.x, b: direction.y }, + { min: 0, max: this.imgH } + ); + coords.x1 = newCoords.a1; + coords.y1 = newCoords.b1; + coords.x2 = newCoords.a2; + coords.y2 = newCoords.b2; + } + + }, + + /** + * Applies the provided ratio to the provided coordinates based on provided direction & bounds, + * use to encapsulate functionality to make it easy to apply to either axis. This is probably + * quite hard to visualise so see the x axis example within applyRatio() + * + * Example in parameter details & comments is for requesting applying ratio to x axis. + * + * @access private + * @param obj Coords object (a1, b1, a2, b2) where a = x & b = y in example + * @param obj Ratio object (a, b) where a = x & b = y in example + * @param obj Direction object (a, b) where a = x & b = y in example + * @param obj Bounds (min, max) + * @return obj Coords object (a1, b1, a2, b2) where a = x & b = y in example + */ + applyRatioToAxis: function( coords, ratio, direction, bounds ) { + var newCoords = Object.extend( coords, {} ); + var calcDimA = newCoords.a2 - newCoords.a1; // calculate dimension a (e.g. width) + var targDimB = Math.floor( calcDimA * ratio.b / ratio.a ); // the target dimension b (e.g. height) + var targB; // to hold target b (e.g. y value) + var targDimA; // to hold target dimension a (e.g. width) + var calcDimB = null; // to hold calculated dimension b (e.g. height) + + // dump( 'newCoords[0]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n'); + + if( direction.b == 1 ) { // if travelling in a positive direction + // make sure we're not going out of bounds + targB = newCoords.b1 + targDimB; + if( targB > bounds.max ) { + targB = bounds.max; + calcDimB = targB - newCoords.b1; // calcuate dimension b (e.g. height) + } + + newCoords.b2 = targB; + } else { // if travelling in a negative direction + // make sure we're not going out of bounds + targB = newCoords.b2 - targDimB; + if( targB < bounds.min ) { + targB = bounds.min; + calcDimB = targB + newCoords.b2; // calcuate dimension b (e.g. height) + } + newCoords.b1 = targB; + } + + // dump( 'newCoords[1]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n'); + + // apply the calculated dimensions + if( calcDimB != null ) { + targDimA = Math.floor( calcDimB * ratio.a / ratio.b ); + + if( direction.a == 1 ) newCoords.a2 = newCoords.a1 + targDimA; + else newCoords.a1 = newCoords.a1 = newCoords.a2 - targDimA; + } + + // dump( 'newCoords[2]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n'); + + return newCoords; + }, + + /** + * Draws the select area + * + * @access private + * @return void + */ + drawArea: function( ) { + /* + * NOTE: I'm not using the Element.setStyle() shortcut as they make it + * quite sluggish on Mac based browsers + */ + // dump( 'drawArea : ' + this.areaCoords.x1 + ',' + this.areaCoords.y1 + ',' + this.areaCoords.x2 + ',' + this.areaCoords.y2 + '\n' ); + var areaWidth = this.calcW(); + var areaHeight = this.calcH(); + + /* + * Calculate all the style strings before we use them, allows reuse & produces quicker + * rendering (especially noticable in Mac based browsers) + */ + var px = 'px'; + var params = [ + this.areaCoords.x1 + px, // the left of the selArea + this.areaCoords.y1 + px, // the top of the selArea + areaWidth + px, // width of the selArea + areaHeight + px, // height of the selArea + this.areaCoords.x2 + px, // bottom of the selArea + this.areaCoords.y2 + px, // right of the selArea + (this.img.width - this.areaCoords.x2) + px, // right edge of selArea + (this.img.height - this.areaCoords.y2) + px // bottom edge of selArea + ]; + + // do the select area + var areaStyle = this.selArea.style; + areaStyle.left = params[0]; + areaStyle.top = params[1]; + areaStyle.width = params[2]; + areaStyle.height = params[3]; + + // position the north, east, south & west handles + var horizHandlePos = Math.ceil( (areaWidth - 6) / 2 ) + px; + var vertHandlePos = Math.ceil( (areaHeight - 6) / 2 ) + px; + + this.handleN.style.left = horizHandlePos; + this.handleE.style.top = vertHandlePos; + this.handleS.style.left = horizHandlePos; + this.handleW.style.top = vertHandlePos; + + // draw the four overlays + this.north.style.height = params[1]; + + var eastStyle = this.east.style; + eastStyle.top = params[1]; + eastStyle.height = params[3]; + eastStyle.left = params[4]; + eastStyle.width = params[6]; + + var southStyle = this.south.style; + southStyle.top = params[5]; + southStyle.height = params[7]; + + var westStyle = this.west.style; + westStyle.top = params[1]; + westStyle.height = params[3]; + westStyle.width = params[0]; + + // call the draw method on sub classes + this.subDrawArea(); + + this.forceReRender(); + }, + + /** + * Force the re-rendering of the selArea element which fixes rendering issues in Safari + * & IE PC, especially evident when re-sizing perfectly vertical using any of the south handles + * + * @access private + * @return void + */ + forceReRender: function() { + if( this.isIE || this.isWebKit) { + var n = document.createTextNode(' '); + var d,el,fixEL,i; + + if( this.isIE ) fixEl = this.selArea; + else if( this.isWebKit ) { + fixEl = document.getElementsByClassName( 'imgCrop_marqueeSouth', this.imgWrap )[0]; + /* we have to be a bit more forceful for Safari, otherwise the the marquee & + * the south handles still don't move + */ + d = Builder.node( 'div', '' ); + d.style.visibility = 'hidden'; + + var classList = ['SE','S','SW']; + for( i = 0; i < classList.length; i++ ) { + el = document.getElementsByClassName( 'imgCrop_handle' + classList[i], this.selArea )[0]; + if( el.childNodes.length ) el.removeChild( el.childNodes[0] ); + el.appendChild(d); + } + } + fixEl.appendChild(n); + fixEl.removeChild(n); + } + }, + + /** + * Starts the resize + * + * @access private + * @param obj Event + * @return void + */ + startResize: function( e ) { + this.startCoords = this.cloneCoords( this.areaCoords ); + + this.resizing = true; + this.resizeHandle = Event.element( e ).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/, ''); + // dump( 'this.resizeHandle : ' + this.resizeHandle + '\n' ); + Event.stop( e ); + }, + + /** + * Starts the drag + * + * @access private + * @param obj Event + * @return void + */ + startDrag: function( e ) { + this.selArea.show(); + this.clickCoords = this.getCurPos( e ); + + this.setAreaCoords( { x1: this.clickCoords.x, y1: this.clickCoords.y, x2: this.clickCoords.x, y2: this.clickCoords.y }, false, false, null ); + + this.dragging = true; + this.onDrag( e ); // incase the user just clicks once after already making a selection + Event.stop( e ); + }, + + /** + * Gets the current cursor position relative to the image + * + * @access private + * @param obj Event + * @return obj x,y pixels of the cursor + */ + getCurPos: function( e ) { + // get the offsets for the wrapper within the document + var el = this.imgWrap, wrapOffsets = Position.cumulativeOffset( el ); + // remove any scrolling that is applied to the wrapper (this may be buggy) - don't count the scroll on the body as that won't affect us + while( el.nodeName != 'BODY' ) { + wrapOffsets[1] -= el.scrollTop || 0; + wrapOffsets[0] -= el.scrollLeft || 0; + el = el.parentNode; + } + return curPos = { + x: Event.pointerX(e) - wrapOffsets[0], + y: Event.pointerY(e) - wrapOffsets[1] + } + }, + + /** + * Performs the drag for both resize & inital draw dragging + * + * @access private + * @param obj Event + * @return void + */ + onDrag: function( e ) { + if( this.dragging || this.resizing ) { + + var resizeHandle = null; + var curPos = this.getCurPos( e ); + var newCoords = this.cloneCoords( this.areaCoords ); + var direction = { x: 1, y: 1 }; + + if( this.dragging ) { + if( curPos.x < this.clickCoords.x ) direction.x = -1; + if( curPos.y < this.clickCoords.y ) direction.y = -1; + + this.transformCoords( curPos.x, this.clickCoords.x, newCoords, 'x' ); + this.transformCoords( curPos.y, this.clickCoords.y, newCoords, 'y' ); + } else if( this.resizing ) { + resizeHandle = this.resizeHandle; + // do x movements first + if( resizeHandle.match(/E/) ) { + // if we're moving an east handle + this.transformCoords( curPos.x, this.startCoords.x1, newCoords, 'x' ); + if( curPos.x < this.startCoords.x1 ) direction.x = -1; + } else if( resizeHandle.match(/W/) ) { + // if we're moving an west handle + this.transformCoords( curPos.x, this.startCoords.x2, newCoords, 'x' ); + if( curPos.x < this.startCoords.x2 ) direction.x = -1; + } + + // do y movements second + if( resizeHandle.match(/N/) ) { + // if we're moving an north handle + this.transformCoords( curPos.y, this.startCoords.y2, newCoords, 'y' ); + if( curPos.y < this.startCoords.y2 ) direction.y = -1; + } else if( resizeHandle.match(/S/) ) { + // if we're moving an south handle + this.transformCoords( curPos.y, this.startCoords.y1, newCoords, 'y' ); + if( curPos.y < this.startCoords.y1 ) direction.y = -1; + } + + } + + this.setAreaCoords( newCoords, false, e.shiftKey, direction, resizeHandle ); + this.drawArea(); + Event.stop( e ); // stop the default event (selecting images & text) in Safari & IE PC + } + }, + + /** + * Applies the appropriate transform to supplied co-ordinates, on the + * defined axis, depending on the relationship of the supplied values + * + * @access private + * @param int Current value of pointer + * @param int Base value to compare current pointer val to + * @param obj Coordinates to apply transformation on x1, x2, y1, y2 + * @param string Axis to apply transformation on 'x' || 'y' + * @return void + */ + transformCoords : function( curVal, baseVal, coords, axis ) { + var newVals = [ curVal, baseVal ]; + if( curVal > baseVal ) newVals.reverse(); + coords[ axis + '1' ] = newVals[0]; + coords[ axis + '2' ] = newVals[1]; + }, + + /** + * Ends the crop & passes the values of the select area on to the appropriate + * callback function on completion of a crop + * + * @access private + * @return void + */ + endCrop : function() { + this.dragging = false; + this.resizing = false; + + this.options.onEndCrop( + this.areaCoords, + { + width: this.calcW(), + height: this.calcH() + } + ); + }, + + /** + * Abstract method called on the end of initialization + * + * @access private + * @abstract + * @return void + */ + subInitialize: function() {}, + + /** + * Abstract method called on the end of drawArea() + * + * @access private + * @abstract + * @return void + */ + subDrawArea: function() {} +}; + + + + +/** + * Extend the Cropper.Img class to allow for presentation of a preview image of the resulting crop, + * the option for displayOnInit is always overridden to true when displaying a preview image + * + * Usage: + * @param obj Image element to attach to + * @param obj Optional options: + * - see Cropper.Img for base options + * + * - previewWrap obj + * HTML element that will be used as a container for the preview image + */ +Cropper.ImgWithPreview = Class.create(); + +Object.extend( Object.extend( Cropper.ImgWithPreview.prototype, Cropper.Img.prototype ), { + + /** + * Implements the abstract method from Cropper.Img to initialize preview image settings. + * Will only attach a preview image is the previewWrap element is defined and the minWidth + * & minHeight options are set. + * + * @see Croper.Img.subInitialize + */ + subInitialize: function() { + /** + * Whether or not we've attached a preview image + * @var boolean + */ + this.hasPreviewImg = false; + if( typeof(this.options.previewWrap) != 'undefined' + && this.options.minWidth > 0 + && this.options.minHeight > 0 + ) { + /** + * The preview image wrapper element + * @var obj HTML element + */ + this.previewWrap = $( this.options.previewWrap ); + /** + * The preview image element + * @var obj HTML IMG element + */ + this.previewImg = this.img.cloneNode( false ); + // set the ID of the preview image to be unique + this.previewImg.id = 'imgCrop_' + this.previewImg.id; + + + // set the displayOnInit option to true so we display the select area at the same time as the thumbnail + this.options.displayOnInit = true; + + this.hasPreviewImg = true; + + this.previewWrap.addClassName( 'imgCrop_previewWrap' ); + + this.previewWrap.setStyle( + { + width: this.options.minWidth + 'px', + height: this.options.minHeight + 'px' + } + ); + + this.previewWrap.appendChild( this.previewImg ); + } + }, + + /** + * Implements the abstract method from Cropper.Img to draw the preview image + * + * @see Croper.Img.subDrawArea + */ + subDrawArea: function() { + if( this.hasPreviewImg ) { + // get the ratio of the select area to the src image + var calcWidth = this.calcW(); + var calcHeight = this.calcH(); + // ratios for the dimensions of the preview image + var dimRatio = { + x: this.imgW / calcWidth, + y: this.imgH / calcHeight + }; + //ratios for the positions within the preview + var posRatio = { + x: calcWidth / this.options.minWidth, + y: calcHeight / this.options.minHeight + }; + + // setting the positions in an obj before apply styles for rendering speed increase + var calcPos = { + w: Math.ceil( this.options.minWidth * dimRatio.x ) + 'px', + h: Math.ceil( this.options.minHeight * dimRatio.y ) + 'px', + x: '-' + Math.ceil( this.areaCoords.x1 / posRatio.x ) + 'px', + y: '-' + Math.ceil( this.areaCoords.y1 / posRatio.y ) + 'px' + } + + var previewStyle = this.previewImg.style; + previewStyle.width = calcPos.w; + previewStyle.height = calcPos.h; + previewStyle.left = calcPos.x; + previewStyle.top = calcPos.y; + } + } + +}); diff --git a/cropper/lib/builder.js b/cropper/lib/builder.js new file mode 100644 index 0000000..5b15ba9 --- /dev/null +++ b/cropper/lib/builder.js @@ -0,0 +1,101 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// See scriptaculous.js for full license. + +var Builder = { + NODEMAP: { + AREA: 'map', + CAPTION: 'table', + COL: 'table', + COLGROUP: 'table', + LEGEND: 'fieldset', + OPTGROUP: 'select', + OPTION: 'select', + PARAM: 'object', + TBODY: 'table', + TD: 'table', + TFOOT: 'table', + TH: 'table', + THEAD: 'table', + TR: 'table' + }, + // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, + // due to a Firefox bug + node: function(elementName) { + elementName = elementName.toUpperCase(); + + // try innerHTML approach + var parentTag = this.NODEMAP[elementName] || 'div'; + var parentElement = document.createElement(parentTag); + try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 + parentElement.innerHTML = "<" + elementName + ">"; + } catch(e) {} + var element = parentElement.firstChild || null; + + // see if browser added wrapping tags + if(element && (element.tagName != elementName)) + element = element.getElementsByTagName(elementName)[0]; + + // fallback to createElement approach + if(!element) element = document.createElement(elementName); + + // abort if nothing could be created + if(!element) return; + + // attributes (or text) + if(arguments[1]) + if(this._isStringOrNumber(arguments[1]) || + (arguments[1] instanceof Array)) { + this._children(element, arguments[1]); + } else { + var attrs = this._attributes(arguments[1]); + if(attrs.length) { + try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 + parentElement.innerHTML = "<" +elementName + " " + + attrs + ">"; + } catch(e) {} + element = parentElement.firstChild || null; + // workaround firefox 1.0.X bug + if(!element) { + element = document.createElement(elementName); + for(attr in arguments[1]) + element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; + } + if(element.tagName != elementName) + element = parentElement.getElementsByTagName(elementName)[0]; + } + } + + // text, or array of children + if(arguments[2]) + this._children(element, arguments[2]); + + return element; + }, + _text: function(text) { + return document.createTextNode(text); + }, + _attributes: function(attributes) { + var attrs = []; + for(attribute in attributes) + attrs.push((attribute=='className' ? 'class' : attribute) + + '="' + attributes[attribute].toString().escapeHTML() + '"'); + return attrs.join(" "); + }, + _children: function(element, children) { + if(typeof children=='object') { // array can hold nodes and text + children.flatten().each( function(e) { + if(typeof e=='object') + element.appendChild(e) + else + if(Builder._isStringOrNumber(e)) + element.appendChild(Builder._text(e)); + }); + } else + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); + }, + _isStringOrNumber: function(param) { + return(typeof param=='string' || typeof param=='number'); + } +} \ No newline at end of file diff --git a/cropper/lib/controls.js b/cropper/lib/controls.js new file mode 100644 index 0000000..de0261e --- /dev/null +++ b/cropper/lib/controls.js @@ -0,0 +1,815 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
    /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}; diff --git a/cropper/lib/dragdrop.js b/cropper/lib/dragdrop.js new file mode 100644 index 0000000..be2a30f --- /dev/null +++ b/cropper/lib/dragdrop.js @@ -0,0 +1,915 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur}); + }, + endeffect: function(element) { + var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0 + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity}); + }, + zindex: 1000, + revert: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } + }, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) { + var h = Element.childrenWithClassName(this.element, options.handle, true); + if(h.length>0) this.handle = h[0]; + } + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) + options.scroll = $(options.scroll); + + Element.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + if(this.element._revert) { + this.element._revert.cancel(); + this.element._revert = null; + } + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + Position.prepare(); + Droppables.show(pointer, this.element); + Draggables.notify('onDrag', this, event); + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft; + p[1] += this.options.scroll.scrollTop; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1],this); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: /^[^_]*_(.*)$/, + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + //greedy: !options.dropOnEmpty + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.childrenWithClassName(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.addClassName(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.left = offsets[0] + 'px'; + Sortable._marker.style.top = offsets[1] + 'px'; + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + else + Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + + Element.show(Sortable._marker); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: new Array, + position: parent.children.length, + container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase()) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + /* Finds the first element of the given tag type within a parent element. + Used for finding the first LI[ST] within a L[IST]I[TEM].*/ + _findChildrenElement: function (element, containerTag) { + if (element && element.hasChildNodes) + for (var i = 0; i < element.childNodes.length; ++i) + if (element.childNodes[i].tagName == containerTag) + return element.childNodes[i]; + + return null; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || {}); + + var root = { + id: null, + parent: null, + children: new Array, + container: element, + position: 0 + } + + return Sortable._tree (element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +/* Returns true if child is contained within element */ +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + + if (child.parentNode == element) return true; + + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + if (type == 'vertical' || type == 'height') + return element.offsetHeight; + else + return element.offsetWidth; +} \ No newline at end of file diff --git a/cropper/lib/effects.js b/cropper/lib/effects.js new file mode 100644 index 0000000..0864323 --- /dev/null +++ b/cropper/lib/effects.js @@ -0,0 +1,958 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + Element.setStyle(element, {fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, 'opacity')) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + Element.setStyle(element, { opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : null }); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + Element.setStyle(element, {opacity: value}); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, + { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.childrenWithClassName = function(element, className, findFirst) { + var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)"); + var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { + return (c.className && c.className.match(classNameRegExp)); + }); + if(!results) results = []; + return results; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + return '#'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: this.options.x * position + this.originalLeft + 'px', + top: this.options.y * position + this.originalTop + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = width + 'px'; + if(this.options.scaleY) d.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: this.element.getStyle('background-image') }; + this.element.setStyle({backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide(); + effect.element.setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from); + effect.element.show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + effect.effects[0].element.setStyle({position: 'absolute'}); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned(); + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.undoPositioned(); + effect.element.setStyle({opacity: oldOpacity}); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned(); + effect.element.setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + element.cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + // IE will crash if child is undoPositioned first + if(/MSIE/.test(navigator.userAgent)){ + effect.element.undoPositioned(); + effect.element.firstChild.undoPositioned(); + }else{ + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + } + effect.element.firstChild.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + element.cleanWhitespace(); + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + effect.element.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + effect.element.hide(effect.element); + effect.element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide(); + effect.element.makeClipping(); + effect.element.makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}); + effect.effects[0].element.show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned(); + effect.effects[0].element.makeClipping(); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file diff --git a/cropper/lib/prototype.js b/cropper/lib/prototype.js new file mode 100644 index 0000000..0caf9cd --- /dev/null +++ b/cropper/lib/prototype.js @@ -0,0 +1,2006 @@ +/* Prototype JavaScript framework, version 1.5.0_rc0 + * (c) 2005 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0_rc0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += (replacement(match) || '').toString(); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + (object[match[3]] || '').toString(); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version, + 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*']; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', this.options.contentType); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval('(' + this.header('X-JSON') + ')'); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $() { + var results = [], element; + for (var i = 0; i < arguments.length; i++) { + element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + results.push(Element.extend(element)); + } + return results.length < 2 ? results[0] : results; +} + +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(Element.extend(child)); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element) return; + if (_nativeExtensions) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +} + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +} + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + replace: function(element, html) { + element = $(element); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + childOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +} + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + var HTMLElement = {} + HTMLElement.prototype = document.createElement('div').__proto__; +} + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + if(typeof HTMLElement != 'undefined') { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + HTMLElement.prototype[property] = cache.findOrStore(value); + } + _nativeExtensions = true; + } +} + +Element.addMethods(); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toLowerCase(); + if (tagName == 'tbody' || tagName == 'tr') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.id == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0; i < clause.length; i++) + conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.getAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push(value + ' != null'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0; i < scope.length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +function $$() { + return $A(arguments).map(function(expression) { + return expression.strip().split(/\s+/).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.map(selector.findElements.bind(selector)).flatten(); + }); + }).flatten(); +} +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (var tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value || opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = []; + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) + value.push(opt.value || opt.text); + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/cropper/lib/scriptaculous.js b/cropper/lib/scriptaculous.js new file mode 100644 index 0000000..f61fc57 --- /dev/null +++ b/cropper/lib/scriptaculous.js @@ -0,0 +1,47 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// 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. + +var Scriptaculous = { + Version: '1.6.1', + require: function(libraryName) { + // inserting via DOM fails in Safari 2.0, so brute force approach + document.write(''); + }, + load: function() { + if((typeof Prototype=='undefined') || + (typeof Element == 'undefined') || + (typeof Element.Methods=='undefined') || + parseFloat(Prototype.Version.split(".")[0] + "." + + Prototype.Version.split(".")[1]) < 1.5) + throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0"); + + $A(document.getElementsByTagName("script")).findAll( function(s) { + return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/)) + }).each( function(s) { + var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,''); + var includes = s.src.match(/\?.*load=([a-z,]*)/); + (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each( + function(include) { Scriptaculous.require(path+include+'.js') }); + }); + } +} + +Scriptaculous.load(); \ No newline at end of file diff --git a/cropper/lib/slider.js b/cropper/lib/slider.js new file mode 100644 index 0000000..c0f1fc0 --- /dev/null +++ b/cropper/lib/slider.js @@ -0,0 +1,283 @@ +// Copyright (c) 2005 Marty Haught, Thomas Fuchs +// +// See http://script.aculo.us for more info +// +// 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. + +if(!Control) var Control = {}; +Control.Slider = Class.create(); + +// options: +// axis: 'vertical', or 'horizontal' (default) +// +// callbacks: +// onChange(value) +// onSlide(value) +Control.Slider.prototype = { + initialize: function(handle, track, options) { + var slider = this; + + if(handle instanceof Array) { + this.handles = handle.collect( function(e) { return $(e) }); + } else { + this.handles = [$(handle)]; + } + + this.track = $(track); + this.options = options || {}; + + this.axis = this.options.axis || 'horizontal'; + this.increment = this.options.increment || 1; + this.step = parseInt(this.options.step || '1'); + this.range = this.options.range || $R(0,1); + + this.value = 0; // assure backwards compat + this.values = this.handles.map( function() { return 0 }); + this.spans = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false; + this.options.startSpan = $(this.options.startSpan || null); + this.options.endSpan = $(this.options.endSpan || null); + + this.restricted = this.options.restricted || false; + + this.maximum = this.options.maximum || this.range.end; + this.minimum = this.options.minimum || this.range.start; + + // Will be used to align the handle onto the track, if necessary + this.alignX = parseInt(this.options.alignX || '0'); + this.alignY = parseInt(this.options.alignY || '0'); + + this.trackLength = this.maximumOffset() - this.minimumOffset(); + this.handleLength = this.isVertical() ? this.handles[0].offsetHeight : this.handles[0].offsetWidth; + + this.active = false; + this.dragging = false; + this.disabled = false; + + if(this.options.disabled) this.setDisabled(); + + // Allowed values array + this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false; + if(this.allowedValues) { + this.minimum = this.allowedValues.min(); + this.maximum = this.allowedValues.max(); + } + + this.eventMouseDown = this.startDrag.bindAsEventListener(this); + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.update.bindAsEventListener(this); + + // Initialize handles in reverse (make sure first handle is active) + this.handles.each( function(h,i) { + i = slider.handles.length-1-i; + slider.setValue(parseFloat( + (slider.options.sliderValue instanceof Array ? + slider.options.sliderValue[i] : slider.options.sliderValue) || + slider.range.start), i); + Element.makePositioned(h); // fix IE + Event.observe(h, "mousedown", slider.eventMouseDown); + }); + + Event.observe(this.track, "mousedown", this.eventMouseDown); + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + + this.initialized = true; + }, + dispose: function() { + var slider = this; + Event.stopObserving(this.track, "mousedown", this.eventMouseDown); + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + this.handles.each( function(h) { + Event.stopObserving(h, "mousedown", slider.eventMouseDown); + }); + }, + setDisabled: function(){ + this.disabled = true; + }, + setEnabled: function(){ + this.disabled = false; + }, + getNearestValue: function(value){ + if(this.allowedValues){ + if(value >= this.allowedValues.max()) return(this.allowedValues.max()); + if(value <= this.allowedValues.min()) return(this.allowedValues.min()); + + var offset = Math.abs(this.allowedValues[0] - value); + var newValue = this.allowedValues[0]; + this.allowedValues.each( function(v) { + var currentOffset = Math.abs(v - value); + if(currentOffset <= offset){ + newValue = v; + offset = currentOffset; + } + }); + return newValue; + } + if(value > this.range.end) return this.range.end; + if(value < this.range.start) return this.range.start; + return value; + }, + setValue: function(sliderValue, handleIdx){ + if(!this.active) { + this.activeHandle = this.handles[handleIdx]; + this.activeHandleIdx = handleIdx; + this.updateStyles(); + } + handleIdx = handleIdx || this.activeHandleIdx || 0; + if(this.initialized && this.restricted) { + if((handleIdx>0) && (sliderValuethis.values[handleIdx+1])) + sliderValue = this.values[handleIdx+1]; + } + sliderValue = this.getNearestValue(sliderValue); + this.values[handleIdx] = sliderValue; + this.value = this.values[0]; // assure backwards compat + + this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = + this.translateToPx(sliderValue); + + this.drawSpans(); + if(!this.dragging || !this.event) this.updateFinished(); + }, + setValueBy: function(delta, handleIdx) { + this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, + handleIdx || this.activeHandleIdx || 0); + }, + translateToPx: function(value) { + return Math.round( + ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * + (value - this.range.start)) + "px"; + }, + translateToValue: function(offset) { + return ((offset/(this.trackLength-this.handleLength) * + (this.range.end-this.range.start)) + this.range.start); + }, + getRange: function(range) { + var v = this.values.sortBy(Prototype.K); + range = range || 0; + return $R(v[range],v[range+1]); + }, + minimumOffset: function(){ + return(this.isVertical() ? this.alignY : this.alignX); + }, + maximumOffset: function(){ + return(this.isVertical() ? + this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX); + }, + isVertical: function(){ + return (this.axis == 'vertical'); + }, + drawSpans: function() { + var slider = this; + if(this.spans) + $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) }); + if(this.options.startSpan) + this.setSpan(this.options.startSpan, + $R(0, this.values.length>1 ? this.getRange(0).min() : this.value )); + if(this.options.endSpan) + this.setSpan(this.options.endSpan, + $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum)); + }, + setSpan: function(span, range) { + if(this.isVertical()) { + span.style.top = this.translateToPx(range.start); + span.style.height = this.translateToPx(range.end - range.start + this.range.start); + } else { + span.style.left = this.translateToPx(range.start); + span.style.width = this.translateToPx(range.end - range.start + this.range.start); + } + }, + updateStyles: function() { + this.handles.each( function(h){ Element.removeClassName(h, 'selected') }); + Element.addClassName(this.activeHandle, 'selected'); + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + if(!this.disabled){ + this.active = true; + + var handle = Event.element(event); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + if(handle==this.track) { + var offsets = Position.cumulativeOffset(this.track); + this.event = event; + this.setValue(this.translateToValue( + (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2) + )); + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + } else { + // find the handle (prevents issues with Safari) + while((this.handles.indexOf(handle) == -1) && handle.parentNode) + handle = handle.parentNode; + + this.activeHandle = handle; + this.activeHandleIdx = this.handles.indexOf(this.activeHandle); + this.updateStyles(); + + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + } + } + Event.stop(event); + } + }, + update: function(event) { + if(this.active) { + if(!this.dragging) this.dragging = true; + this.draw(event); + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + Event.stop(event); + } + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.track); + pointer[0] -= this.offsetX + offsets[0]; + pointer[1] -= this.offsetY + offsets[1]; + this.event = event; + this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] )); + if(this.initialized && this.options.onSlide) + this.options.onSlide(this.values.length>1 ? this.values : this.value, this); + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + finishDrag: function(event, success) { + this.active = false; + this.dragging = false; + this.updateFinished(); + }, + updateFinished: function() { + if(this.initialized && this.options.onChange) + this.options.onChange(this.values.length>1 ? this.values : this.value, this); + this.event = null; + } +} \ No newline at end of file diff --git a/cropper/lib/unittest.js b/cropper/lib/unittest.js new file mode 100644 index 0000000..d2c2d81 --- /dev/null +++ b/cropper/lib/unittest.js @@ -0,0 +1,383 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/) +// +// 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. + + +// experimental, Firefox-only +Event.simulateMouse = function(element, eventName) { + var options = Object.extend({ + pointerX: 0, + pointerY: 0, + buttons: 0 + }, arguments[2] || {}); + var oEvent = document.createEvent("MouseEvents"); + oEvent.initMouseEvent(eventName, true, true, document.defaultView, + options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, + false, false, false, false, 0, $(element)); + + if(this.mark) Element.remove(this.mark); + this.mark = document.createElement('div'); + this.mark.appendChild(document.createTextNode(" ")); + document.body.appendChild(this.mark); + this.mark.style.position = 'absolute'; + this.mark.style.top = options.pointerY + "px"; + this.mark.style.left = options.pointerX + "px"; + this.mark.style.width = "5px"; + this.mark.style.height = "5px;"; + this.mark.style.borderTop = "1px solid red;" + this.mark.style.borderLeft = "1px solid red;" + + if(this.step) + alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options)); + + $(element).dispatchEvent(oEvent); +}; + +// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2. +// You need to downgrade to 1.0.4 for now to get this working +// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much +Event.simulateKey = function(element, eventName) { + var options = Object.extend({ + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + keyCode: 0, + charCode: 0 + }, arguments[2] || {}); + + var oEvent = document.createEvent("KeyEvents"); + oEvent.initKeyEvent(eventName, true, true, window, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, + options.keyCode, options.charCode ); + $(element).dispatchEvent(oEvent); +}; + +Event.simulateKeys = function(element, command) { + for(var i=0; i' + + '' + + '' + + '' + + '
    StatusTestMessage
    '; + this.logsummary = $('logsummary') + this.loglines = $('loglines'); + }, + _toHTML: function(txt) { + return txt.escapeHTML().replace(/\n/g,"
    "); + } +} + +Test.Unit.Runner = Class.create(); +Test.Unit.Runner.prototype = { + initialize: function(testcases) { + this.options = Object.extend({ + testLog: 'testlog' + }, arguments[1] || {}); + this.options.resultsURL = this.parseResultsURLQueryParameter(); + if (this.options.testLog) { + this.options.testLog = $(this.options.testLog) || null; + } + if(this.options.tests) { + this.tests = []; + for(var i = 0; i < this.options.tests.length; i++) { + if(/^test/.test(this.options.tests[i])) { + this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); + } + } + } else { + if (this.options.test) { + this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])]; + } else { + this.tests = []; + for(var testcase in testcases) { + if(/^test/.test(testcase)) { + this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"])); + } + } + } + } + this.currentTest = 0; + this.logger = new Test.Unit.Logger(this.options.testLog); + setTimeout(this.runTests.bind(this), 1000); + }, + parseResultsURLQueryParameter: function() { + return window.location.search.parseQuery()["resultsURL"]; + }, + // Returns: + // "ERROR" if there was an error, + // "FAILURE" if there was a failure, or + // "SUCCESS" if there was neither + getResult: function() { + var hasFailure = false; + for(var i=0;i 0) { + return "ERROR"; + } + if (this.tests[i].failures > 0) { + hasFailure = true; + } + } + if (hasFailure) { + return "FAILURE"; + } else { + return "SUCCESS"; + } + }, + postResults: function() { + if (this.options.resultsURL) { + new Ajax.Request(this.options.resultsURL, + { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false }); + } + }, + runTests: function() { + var test = this.tests[this.currentTest]; + if (!test) { + // finished! + this.postResults(); + this.logger.summary(this.summary()); + return; + } + if(!test.isWaiting) { + this.logger.start(test.name); + } + test.run(); + if(test.isWaiting) { + this.logger.message("Waiting for " + test.timeToWait + "ms"); + setTimeout(this.runTests.bind(this), test.timeToWait || 1000); + } else { + this.logger.finish(test.status(), test.summary()); + this.currentTest++; + // tail recursive, hopefully the browser will skip the stackframe + this.runTests(); + } + }, + summary: function() { + var assertions = 0; + var failures = 0; + var errors = 0; + var messages = []; + for(var i=0;i 0) return 'failed'; + if (this.errors > 0) return 'error'; + return 'passed'; + }, + assert: function(expression) { + var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"'; + try { expression ? this.pass() : + this.fail(message); } + catch(e) { this.error(e); } + }, + assertEqual: function(expected, actual) { + var message = arguments[2] || "assertEqual"; + try { (expected == actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertEnumEqual: function(expected, actual) { + var message = arguments[2] || "assertEnumEqual"; + try { $A(expected).length == $A(actual).length && + expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? + this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + + ', actual ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, + assertNotEqual: function(expected, actual) { + var message = arguments[2] || "assertNotEqual"; + try { (expected != actual) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertNull: function(obj) { + var message = arguments[1] || 'assertNull' + try { (obj==null) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); } + catch(e) { this.error(e); } + }, + assertHidden: function(element) { + var message = arguments[1] || 'assertHidden'; + this.assertEqual("none", element.style.display, message); + }, + assertNotNull: function(object) { + var message = arguments[1] || 'assertNotNull'; + this.assert(object != null, message); + }, + assertInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertInstanceOf'; + try { + (actual instanceof expected) ? this.pass() : + this.fail(message + ": object was not an instance of the expected type"); } + catch(e) { this.error(e); } + }, + assertNotInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertNotInstanceOf'; + try { + !(actual instanceof expected) ? this.pass() : + this.fail(message + ": object was an instance of the not expected type"); } + catch(e) { this.error(e); } + }, + _isVisible: function(element) { + element = $(element); + if(!element.parentNode) return true; + this.assertNotNull(element); + if(element.style && Element.getStyle(element, 'display') == 'none') + return false; + + return this._isVisible(element.parentNode); + }, + assertNotVisible: function(element) { + this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1])); + }, + assertVisible: function(element) { + this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1])); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; + } +} + +Test.Unit.Testcase = Class.create(); +Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { + initialize: function(name, test, setup, teardown) { + Test.Unit.Assertions.prototype.initialize.bind(this)(); + this.name = name; + this.test = test || function() {}; + this.setup = setup || function() {}; + this.teardown = teardown || function() {}; + this.isWaiting = false; + this.timeToWait = 1000; + }, + wait: function(time, nextPart) { + this.isWaiting = true; + this.test = nextPart; + this.timeToWait = time; + }, + run: function() { + try { + try { + if (!this.isWaiting) this.setup.bind(this)(); + this.isWaiting = false; + this.test.bind(this)(); + } finally { + if(!this.isWaiting) { + this.teardown.bind(this)(); + } + } + } + catch(e) { this.error(e); } + } +}); diff --git a/cropper/licence.txt b/cropper/licence.txt new file mode 100644 index 0000000..b59e029 --- /dev/null +++ b/cropper/licence.txt @@ -0,0 +1,12 @@ +Copyright (c) 2006, David Spurr (www.defusion.org.uk) +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 David Spurr 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. + +http://www.opensource.org/licenses/bsd-license.php \ No newline at end of file diff --git a/cropper/marqueeHoriz.gif b/cropper/marqueeHoriz.gif new file mode 100644 index 0000000000000000000000000000000000000000..25317e5738bfcce43707ec0a0640533f69eb8fff GIT binary patch literal 1125 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 PPffC!10NW#8L$7@U6jV2 literal 0 HcmV?d00001 diff --git a/cropper/tests/castleMed.jpg b/cropper/tests/castleMed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c35a6f554a21eb62f3c3e3580e616a4feee8748c GIT binary patch literal 50584 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%0Dl3VwBB+R7y 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 RJ3V^U{{ZIRKl@F8|Jg7^TQ~p! literal 0 HcmV?d00001 diff --git a/cropper/tests/example-Basic.htm b/cropper/tests/example-Basic.htm new file mode 100644 index 0000000..391c2ec --- /dev/null +++ b/cropper/tests/example-Basic.htm @@ -0,0 +1,106 @@ + + + + + + Basic cropper test + + + + + + + + + + +

    Basic cropper test

    +

    + Some test content before the image +

    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-CSS-Absolute.htm b/cropper/tests/example-CSS-Absolute.htm new file mode 100644 index 0000000..17e4c48 --- /dev/null +++ b/cropper/tests/example-CSS-Absolute.htm @@ -0,0 +1,162 @@ + + + + + + CSS - Absolute positioned (and draggable) test + + + + + + + + + + +

    CSS - Absolute positioned (and draggable) test

    +

    + Some test content before the image +

    +

    + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin. +

    +

    + Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam. +

    +

    + Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi. +

    +

    + In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam. + +

    +

    + Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede. +

    +

    + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin. +

    +

    + Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam. +

    +

    + Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi. +

    +

    + In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam. + +

    +

    + Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede. +

    +

    + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin. +

    +

    + Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam. +

    +

    + Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi. +

    +

    + In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam. + +

    +

    + Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede. +

    + + +
    +

    Absolute test

    +
    + test image +
    + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +
    + + + + + diff --git a/cropper/tests/example-CSS-Float.htm b/cropper/tests/example-CSS-Float.htm new file mode 100644 index 0000000..5066553 --- /dev/null +++ b/cropper/tests/example-CSS-Float.htm @@ -0,0 +1,124 @@ + + + + + + CSS - Float test + + + + + + + + + + +

    Test page with floating wrapper

    +

    + Some test content before the image +

    + +
    +

    Float test

    +
    + test image +
    + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +
    + + + + + diff --git a/cropper/tests/example-CSS-Relative.htm b/cropper/tests/example-CSS-Relative.htm new file mode 100644 index 0000000..5894fe1 --- /dev/null +++ b/cropper/tests/example-CSS-Relative.htm @@ -0,0 +1,116 @@ + + + + + + CSS - Relative test + + + + + + + + + + +

    Test page with relatively positioned wrapper

    +

    + Some test content before the image +

    + +
    +

    Relative test

    +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +
    + + + + + diff --git a/cropper/tests/example-CoordsOnLoad.htm b/cropper/tests/example-CoordsOnLoad.htm new file mode 100644 index 0000000..254a234 --- /dev/null +++ b/cropper/tests/example-CoordsOnLoad.htm @@ -0,0 +1,108 @@ + + + + + + Loading & displaying co-ordinates of crop area on attachment test + + + + + + + + + + +

    Loading & displaying co-ordinates of crop area on attachment test

    +

    + Some test content before the image +

    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-CoordsOnLoadWithRatio.htm b/cropper/tests/example-CoordsOnLoadWithRatio.htm new file mode 100644 index 0000000..3a69636 --- /dev/null +++ b/cropper/tests/example-CoordsOnLoadWithRatio.htm @@ -0,0 +1,109 @@ + + + + + + Loading & displaying co-ordinates (with ratio) of crop area on attachment test< + + + + + + + + + + +

    Loading & displaying co-ordinates (with ratio) of crop area on attachment test

    +

    + Some test content before the image +

    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-Dimensions.htm b/cropper/tests/example-Dimensions.htm new file mode 100644 index 0000000..f54f996 --- /dev/null +++ b/cropper/tests/example-Dimensions.htm @@ -0,0 +1,225 @@ + + + + + + Different dimensions test + + + + + + + + + + +

    Multiple dimensions tests

    +

    + Test of applying different dimension restrictions to the cropper +

    + +
    +
    + Set the cropper with the following dimension restrictions: +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + +
    +
    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-DynamicImage.htm b/cropper/tests/example-DynamicImage.htm new file mode 100644 index 0000000..8983634 --- /dev/null +++ b/cropper/tests/example-DynamicImage.htm @@ -0,0 +1,203 @@ + + + + + + Dynamic image test + + + + + + + + + + +

    Dynamic image test

    +

    + Test of dynamically changing images or removing & re-applying the cropper +

    + +
    + test image +
    + +

    + + +

    + +

    + + +

    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-FixedRatio.htm b/cropper/tests/example-FixedRatio.htm new file mode 100644 index 0000000..973bedd --- /dev/null +++ b/cropper/tests/example-FixedRatio.htm @@ -0,0 +1,104 @@ + + + + + + Fixed ratio test + + + + + + + + + + +

    Fixed ratio test

    +

    + Test of applying a fixed ratio to the cropper +

    +
    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-MinimumDimensions.htm b/cropper/tests/example-MinimumDimensions.htm new file mode 100644 index 0000000..3ae93c8 --- /dev/null +++ b/cropper/tests/example-MinimumDimensions.htm @@ -0,0 +1,105 @@ + + + + + + Min dimensions test + + + + + + + + + + +

    Minimum (both axes ) dimension test

    +

    + Test of applying a minimum dimension to both axes to the cropper +

    +
    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-MinimumWidth.htm b/cropper/tests/example-MinimumWidth.htm new file mode 100644 index 0000000..b0576b8 --- /dev/null +++ b/cropper/tests/example-MinimumWidth.htm @@ -0,0 +1,105 @@ + + + + + + Min (single axis) dimensions test + + + + + + + + + + +

    Minimum (single axis) dimension test

    +

    + Test of applying a minimum dimension to only one axis (width in this case) to the cropper +

    +
    +

    + +
    + test image +
    + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + + + + + diff --git a/cropper/tests/example-Preview.htm b/cropper/tests/example-Preview.htm new file mode 100644 index 0000000..701670c --- /dev/null +++ b/cropper/tests/example-Preview.htm @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + +

    + +
    + test image +
    + +
    + +
    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +
    + + + + + diff --git a/cropper/tests/poppy.jpg b/cropper/tests/poppy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1f6498584473b632b707c83ec751499878266915 GIT binary patch literal 18338 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 literal 0 HcmV?d00001 diff --git a/cropper/tests/staticHTMLStructure.htm b/cropper/tests/staticHTMLStructure.htm new file mode 100644 index 0000000..ddb9927 --- /dev/null +++ b/cropper/tests/staticHTMLStructure.htm @@ -0,0 +1,236 @@ + + + + + + + + + + + +

    + + +

    + test image +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +

    Preview:

    +
    + test image +
    +
    + + + + diff --git a/favicon.gif b/favicon.gif new file mode 100644 index 0000000..e69de29 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/images/b_block.gif b/images/b_block.gif new file mode 100644 index 0000000000000000000000000000000000000000..3bc7c056b50a3b0a89384cf2fed4e2afd1275133 GIT binary patch literal 83 zcmZ?wbhEHb6krfwXkcVGv%d5H|Nn|VSs1w(7#VaJfB+=Jz@*h9&v0Vd&ba1~dF_v# mcD`F(IsL}Og;O-2Pg|Au{n$F?<0WgGew3f)?bBdjum%8!Xdo;A literal 0 HcmV?d00001 diff --git a/images/b_drop.gif b/images/b_drop.gif new file mode 100644 index 0000000000000000000000000000000000000000..b08c68b62d7b7cd7c3329f62863f91b87a0a8387 GIT binary patch literal 138 zcmZ?wbhEHb6krfwc+9~tGcE1^OymD)3}=ju&oD6jXJDAg00RGU0L7myj9d)t3_2i1 zATt2bzxwz1_1Mi5Jei0#Y0$L4LviA%Njt^WI31l&6bhNX4zB1b1cz#v=#L9XQ1BeB*}A z%q#-T*4)b4(o?t>F*0#zoIb=Q$oTmtkhx|Ks|wRYkOCL`0G3nf>>LU!8g&ecZvf@i zF{v>LIB*y*Py(9O5Wuep(t6;`5vIViTuKZ~9gTA(-x+M+aA06$eNbb-z@e}}OvNsM m(}BTo!EZ5z1_KrbCLV@_U2LV?JKcpq4)=8Rb6Mw<&;$Tia9maZ literal 0 HcmV?d00001 diff --git a/images/b_edit.gif b/images/b_edit.gif new file mode 100644 index 0000000000000000000000000000000000000000..79cb3c14449e3398270d7c68e4d9c4acaf0270c6 GIT binary patch literal 311 zcmZ?wbhEHb6krfwSgOvDl{ItSx<|80IG5E3%wk}8TN=B08N(qX_OFK+ejaBFi{rP` zV|%bn@~pMylvykXmNK3&;#()l@-RU2LxS~rOV#6=qM!CN95UhDt;#;HjPvi}p1LfN z>;Qqwtqd3EF?6)EACDCMe}-YrLdLqf4GatnXU_beIrGea>H)={ER0+X_6#~e!+|~$ zU|?ViIWWJ#Lr1EA%BhPtF8COne%Ks1Ax7!qL`B9X8IIF|Ck35K4!9SdRM}K>+{$oq9z#bv`|(K8|7RH1EM%;!+W@rl%$ff) zXP){0|Noy*%+B9=lNdxR0lGTCPZRLGK012lzcvQJL|i0{bqbP0l+XkKJS4LG literal 0 HcmV?d00001 diff --git a/images/default-profile-sm.jpg b/images/default-profile-sm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..348957fb4f52279cb63f985a4c68bd22bb883559 GIT binary patch literal 346 zcmex=LJ%Z3btM5{dxG z5Q+={Y5sqJL6CzXfFXdHQHg;`kdaxC@&6G9QJ~`(PystoSVRC_lmRFz1~LatF$YMs iAetyJFm5sMFf#(}VHRYtXXtL2y{!RNBUT>s|C<0Ppd*j~ literal 0 HcmV?d00001 diff --git a/images/default-profile.jpg b/images/default-profile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85fbca8cd755eb2c076c64e0e09b210bb07aca62 GIT binary patch literal 490 zcmex=LJ%Z3btM5{dxG z5Q+={Y5sqJL6C!CJ;QotMkNL&K}Kdl#{WkcM1hWDKn3hTVG#jzQ3jx>7|0wn#T+2j jf@q?^z_`W0!^{Y@hgp!po}s&8_O=F8jYERR{Qo8Z0X!s5 literal 0 HcmV?d00001 diff --git a/images/larrw.gif b/images/larrw.gif new file mode 100644 index 0000000000000000000000000000000000000000..08902d772a4d22c5a9ca1a001604c0b181e9ba5c GIT binary patch literal 1004 zcmVWr~W5udlDv)YJd~fWW}OfPeu10RMmh zfB^sh0Dyo20RaL60s{jB1qB5L1_lQQ2M7oV2?+@b3JMDg3k(bl4Gj$r4h|0w4-gO# z5fKp*5)u;=6BQK|78Vv47Z(^97#SHE8X6iK8yg%P9334U9v&VaA0HqfAR!?kA|fIq zBO@gxB_<{&CnqN;C@3i@DJm)|D=RB3EG#W8EiNuDFE1}JFfcJOF)}hTGcz+aH8nOi zHa9mnI5;>tIXOByIy*Z%JUl!-Jv}}?K0iM{KtMo2K|w-7LPJACMMXtMMn*?RM@UFW zNl8gcN=i#hOH52mO-)TsPEJoxPf$=$QBhG+Qc_b>Q&m+}R#sM5S65hASXo(FT3T9L zTU%UQTwPsVUS3{bUteHgU}0flVq#)rV`F7yWoBk(XJ=<Cc=sHmx_sj8}~tE;Q5tgNlAt*)-FudlDLu&}YQv9hwVv$M0cwY9dkwzs#p zxVX5vxw*Q!y1To(yu7@dCU$jHda z$;ryf%FD~k%*@Qq&CSlv&d<-!(9qD)(b3Y<($mw^)YR0~)z#M4*4Nk9*x1lt)=I7_<=;-L_>FMg~>g((4 z?Ck9A?d|UF?(gsK@bK{Q@$vHV^7Hfa^z`)g_4W4l_V@Sq`1ttw`T6?#`uqF){QUg= z{r&#_{{R2~A^8LW00930EC2ui03QG!000Qd0RIUbNU)&6g9sA_2#~O$!i5ea5?m;N zp~Q$3D_(R+ae&5+2s?VT2(n;D0wYVB3?N_t0hARN%A7efBukesJqp;#vnNl8D_g!S zfO3G*qezi{9C(sI0irLXN}ZaL>CK@}vufS?6eh{0T*GelDiEyLvqY<2b&B>Z*?(r+ za(xSTt=zd)%i6u0m9Ac)Y^MsAn%64ev`G^mT{>|sOrMZ<0yWrlB+bi{51uTn@~=_G aojrpNT~{(`$}CBqwyJuyUe~7s0suR3`s0TH literal 0 HcmV?d00001 diff --git a/images/rarrw.gif b/images/rarrw.gif new file mode 100644 index 0000000000000000000000000000000000000000..849238c2dcb324e89dfcb3f0ff505899dc4077ae GIT binary patch literal 999 zcmVWr~W5udlDv)YJd~fWW}OfPeu10RMmh zfB^sh0Dyo20RaL60s{jB1qB5L1_lQQ2M7oV2?+@b3JMDg3k(bl4Gj$r4h|0w4-gO# z5fKp*5)u;=6BQK|78Vv47Z(^97#SHE8X6iK8yg%P9334U9v&VaA0HqfAR!?kA|fIq zBO@gxB_<{&CnqN;C@3i@DJm)|D=RB3EG#W8EiNuDFE1}JFfcJOF)}hTGcz+aH8nOi zHa9mnI5;>tIXOByIy*Z%JUl!-Jv}}?K0iM{KtMo2K|w-7LPJACMMXtMMn*?RM@UFW zNl8gcN=i#hOH52mO-)TsPEJoxPf$=$QBhG+Qc_b>Q&m+}R#sM5S65hASXo(FT3T9L zTU%UQTwPsVUS3{bUteHgU}0flVq#)rV`F7yWoBk(XJ=<Cc=sHmx_sj8}~tE;Q5tgNlAt*)-FudlDLu&}YQv9hwVv$M0cwY9dkwzs#p zxVX5vxw*Q!y1To(yu7@dCU$jHda z$;ryf%FD~k%*@Qq&CSlv&d<-!(9qD)(b3Y<($mw^)YR0~)z#M4*4Nk9*x1lt)=I7_<=;-L_>FMg~>g((4 z?Ck9A?d|UF?(gsK@bK{Q@$vHV^7Hfa^z`)g_4W4l_V@Sq`1ttw`T6?#`uqF){QUg= z{r&#_{{R2~A^8LW000L7EC2ui03QG!000QY01pTpNU)&6g9sBI2oOM_Lxc<)KAb2p zfB=RRCtAEHF=IlD12}s8C}2QHj3W(#Byh6h!H6(pGAv1erO1~X1?b$#Q|Ex1Hc=J? zX|kx%qXY`nY^jna(5Fyq7El`0Q(5)ORjwkpAuQ3K!AJCNwn!zPmwE}XNcg{DuZKJ76e06T#T<+lI; literal 0 HcmV?d00001 diff --git a/include/Photo.php b/include/Photo.php new file mode 100644 index 0000000..95ccccc --- /dev/null +++ b/include/Photo.php @@ -0,0 +1,171 @@ +image = @imagecreatefromstring($data); + if($this->image !== FALSE) { + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + } + + public function __destruct() { + if($this->image) + imagedestroy($this->image); + } + + public function getWidth() { + return $this->width; + } + + public function getHeight() { + return $this->height; + } + + public function getImage() { + return $this->image; + } + + public function scaleImage($max) { + + $width = $this->width; + $height = $this->height; + + $dest_width = $dest_height = 0; + + if((! $width)|| (! $height)) + return FALSE; + + if($width > $max && $height > $max) { + if($width > $height) { + $dest_width = $max; + $dest_height = intval(( $height * $max ) / $width); + } + else { + $dest_width = intval(( $width * $max ) / $height); + $dest_height = $max; + } + } + else { + if( $width > $max ) { + $dest_width = $max; + $dest_height = intval(( $height * $max ) / $width); + } + else { + if( $height > $max ) { + $dest_width = intval(( $width * $max ) / $height); + $dest_height = $max; + } + else { + $dest_width = $width; + $dest_height = $height; + } + } + } + + + $dest = imagecreatetruecolor( $dest_width, $dest_height ); + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); + if($this->image) + imagedestroy($this->image); + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + + } + + + + public function scaleImageUp($min) { + + $width = $this->width; + $height = $this->height; + + $dest_width = $dest_height = 0; + + if((! $width)|| (! $height)) + return FALSE; + + if($width < $min && $height < $min) { + if($width > $height) { + $dest_width = $min; + $dest_height = intval(( $height * $min ) / $width); + } + else { + $dest_width = intval(( $width * $min ) / $height); + $dest_height = $min; + } + } + else { + if( $width < $min ) { + $dest_width = $min; + $dest_height = intval(( $height * $min ) / $width); + } + else { + if( $height < $min ) { + $dest_width = intval(( $width * $min ) / $height); + $dest_height = $min; + } + else { + $dest_width = $width; + $dest_height = $height; + } + } + } + + + $dest = imagecreatetruecolor( $dest_width, $dest_height ); + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); + if($this->image) + imagedestroy($this->image); + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + + } + + + + public function scaleImageSquare($dim) { + + $dest = imagecreatetruecolor( $dim, $dim ); + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height); + if($this->image) + imagedestroy($this->image); + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + + public function cropImage($max,$x,$y,$w,$h) { + $dest = imagecreatetruecolor( $max, $max ); + imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h); + if($this->image) + imagedestroy($this->image); + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + public function saveImage($path) { + imagejpeg($this->image,$path,100); + } + + public function imageString() { + ob_start(); + imagejpeg($this->image,NULL,100); + $s = ob_get_contents(); + ob_end_clean(); + return $s; + } + + +}} + diff --git a/include/Scrape.php b/include/Scrape.php new file mode 100644 index 0000000..cc50151 --- /dev/null +++ b/include/Scrape.php @@ -0,0 +1,80 @@ +getElementsByTagName('link'); + + // get DFRN link elements + + foreach($items as $item) { + $x = $item->getAttribute('rel'); + if(substr($x,0,5) == "dfrn-") + $ret[$x] = $item->getAttribute('href'); + } + + // Pull out hCard profile elements + + $items = $dom->getElementsByTagName('*'); + foreach($items as $item) { + if(attribute_contains($item->getAttribute('class'), 'vcard')) { + $level2 = $item->getElementsByTagName('*'); + foreach($level2 as $x) { + if(attribute_contains($x->getAttribute('class'),'fn')) + $ret['fn'] = $x->textContent; + if(attribute_contains($x->getAttribute('class'),'photo')) + $ret['photo'] = $x->getAttribute('src'); + if(attribute_contains($x->getAttribute('class'),'key')) + $ret['key'] = $x->textContent; + } + } + } + + return $ret; +}} + + + + + + +if(! function_exists('validate_dfrn')) { +function validate_dfrn($a) { + $errors = 0; + if(! x($a,'key')) + $errors ++; + if(! x($a,'dfrn-request')) + $errors ++; + if(! x($a,'dfrn-confirm')) + $errors ++; + if(! x($a,'dfrn-notify')) + $errors ++; + if(! x($a,'dfrn-poll')) + $errors ++; + return $errors; +}} + + + diff --git a/include/bbcode.php b/include/bbcode.php new file mode 100644 index 0000000..60809a7 --- /dev/null +++ b/include/bbcode.php @@ -0,0 +1,105 @@ +", ">", $Text); + + // Convert new line chars to html
    tags + $Text = nl2br($Text); + + // Set up the parameters for a URL search string + $URLSearchString = " a-zA-Z0-9\:\/\-\?\&\.\=\_\~\#\'"; + // Set up the parameters for a MAIL search string + $MAILSearchString = $URLSearchString . " a-zA-Z0-9\.@"; + + // Perform URL Search + $Text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/", '
    $1', $Text); + $Text = preg_replace("(\[url\=([$URLSearchString]*)\](.+?)\[/url\])", '$2', $Text); + //$Text = preg_replace("(\[url\=([$URLSearchString]*)\]([$URLSearchString]*)\[/url\])", '$2', $Text); + + // Perform MAIL Search + $Text = preg_replace("(\[mail\]([$MAILSearchString]*)\[/mail\])", '$1', $Text); + $Text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.+?)\[\/mail\]/", '$2', $Text); + + // Check for bold text + $Text = preg_replace("(\[b\](.+?)\[\/b])is",'$1',$Text); + + // Check for Italics text + $Text = preg_replace("(\[i\](.+?)\[\/i\])is",'$1',$Text); + + // Check for Underline text + $Text = preg_replace("(\[u\](.+?)\[\/u\])is",'$1',$Text); + + // Check for strike-through text + $Text = preg_replace("(\[s\](.+?)\[\/s\])is",'$1',$Text); + + // Check for over-line text + $Text = preg_replace("(\[o\](.+?)\[\/o\])is",'$1',$Text); + + // Check for colored text + $Text = preg_replace("(\[color=(.+?)\](.+?)\[\/color\])is","$2",$Text); + + // Check for sized text + $Text = preg_replace("(\[size=(.+?)\](.+?)\[\/size\])is","$2",$Text); + + // Check for list text + $Text = preg_replace("/\[list\](.+?)\[\/list\]/is", '
      $1
    ' ,$Text); + $Text = preg_replace("/\[list=1\](.+?)\[\/list\]/is", '
      $1
    ' ,$Text); + $Text = preg_replace("/\[list=i\](.+?)\[\/list\]/s",'
      $1
    ' ,$Text); + $Text = preg_replace("/\[list=I\](.+?)\[\/list\]/s", '
      $1
    ' ,$Text); + $Text = preg_replace("/\[list=a\](.+?)\[\/list\]/s", '
      $1
    ' ,$Text); + $Text = preg_replace("/\[list=A\](.+?)\[\/list\]/s", '
      $1
    ' ,$Text); + $Text = str_replace("[*]", "
  • ", $Text); + + // Check for font change text + $Text = preg_replace("(\[font=(.+?)\](.+?)\[\/font\])","$2",$Text); + + // Declare the format for [code] layout + $CodeLayout = ' + + + + + + +
    Code:
    $1
    '; + // Check for [code] text + $Text = preg_replace("/\[code\](.+?)\[\/code\]/is","$CodeLayout", $Text); + // Declare the format for [php] layout + $phpLayout = ' + + + + + + +
    Code:
    $1
    '; + // Check for [php] text + $Text = preg_replace("/\[php\](.+?)\[\/php\]/is",$phpLayout, $Text); + + // Declare the format for [quote] layout + $QuoteLayout = ' + + + + + + +
    Quote:
    $1
    '; + + // Check for [quote] text + $Text = preg_replace("/\[quote\](.+?)\[\/quote\]/is","$QuoteLayout", $Text); + + // Images + // [img]pathtoimage[/img] + $Text = preg_replace("/\[img\](.+?)\[\/img\]/", '', $Text); + + // [img=widthxheight]image source[/img] + $Text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.+?)\[\/img\]/", '', $Text); + + return $Text; + } diff --git a/include/datetime.php b/include/datetime.php new file mode 100644 index 0000000..f75193b --- /dev/null +++ b/include/datetime.php @@ -0,0 +1,145 @@ +'; + + usort($timezone_identifiers, 'timezone_cmp'); + $continent = ''; + foreach($timezone_identifiers as $value) { + $ex = explode("/", $value); + if(count($ex) > 1) { + if($ex[0] != $continent) { + if($continent != '') + $o .= ''; + $continent = $ex[0]; + $o .= ""; + } + if(count($ex) > 2) + $city = substr($value,strpos($value,'/')+1); + else + $city = $ex[1]; + } + else { + $city = $ex[0]; + if($continent != 'Miscellaneous') { + $o .= ''; + $continent = 'Miscellaneous'; + $o .= ""; + } + } + $city = str_replace('_', ' ', $city); + $selected = (($value == $current) ? " selected=\"selected\" " : ""); + $o .= ""; + } + $o .= ''; + return $o; +}} + + +if(! function_exists('datetime_convert')) { +function datetime_convert($from = 'UTC', $to = 'UTC', $s = 'now', $fmt = "Y-m-d H:i:s") { + $d = new DateTime($s, new DateTimeZone($from)); + $d->setTimeZone(new DateTimeZone($to)); + return($d->format($fmt)); +}} + + + +if(! function_exists('datesel')) { +function datesel($pre,$ymin,$ymax,$allow_blank,$y,$m,$d) { + + $o = ''; + $o .= "--"; + return $o; +}} + + +// TODO rewrite this buggy sucker +function relative_date($posted_date) { + + $localtime = datetime_convert('UTC',date_default_timezone_get(),$posted_date); + + $in_seconds = strtotime($localtime); + + $diff = time() - $in_seconds; + + $months = floor($diff/2592000); + $diff -= $months*2419200; + $weeks = floor($diff/604800); + $diff -= $weeks*604800; + $days = floor($diff/86400); + $diff -= $days*86400; + $hours = floor($diff/3600); + $diff -= $hours*3600; + $minutes = floor($diff/60); + $diff -= $minutes*60; + $seconds = $diff; + + + if ($months>0) { + // over a month old, + return 'over a month ago'; + } else { + if ($weeks>0) { + // weeks and days + $relative_date .= ($relative_date?', ':'').$weeks.' week'.($weeks!=1 ?'s':''); + + } elseif ($days>0) { + // days and hours + $relative_date .= ($relative_date?', ':'').$days.' day'.($days!=1?'s':''); + + } elseif ($hours>0) { + // hours and minutes + $relative_date .= ($relative_date?', ':'').$hours.' hour'.($hours!=1?'s':''); + + } elseif ($minutes>0) { + // minutes only + $relative_date .= ($relative_date?', ':'').$minutes.' minute'.($minutes!=1?'s':''); + } else { + // seconds only + $relative_date .= ($relative_date?', ':'').$seconds.' second'.($seconds!=1?'s':''); + } + } + // show relative date and add proper verbiage + return $relative_date.' ago'; +} diff --git a/include/dba.php b/include/dba.php new file mode 100644 index 0000000..3cc41eb --- /dev/null +++ b/include/dba.php @@ -0,0 +1,138 @@ +db = @new mysqli($server,$user,$pass,$db); + if((mysqli_connect_errno()) && (! install)) + system_unavailable(); + } + + public function q($sql) { + global $debug_text; + + if(! $this->db ) + return false; + + $result = @$this->db->query($sql); + + if($this->debug) { + + $mesg = ''; + + if($this->db->mysqli->errno) + $debug_text .= $this->db->mysqli->error . EOL; + + if($result === false) + $mesg = 'false'; + elseif($result === true) + $mesg = 'true'; + else + $mesg = $result->num_rows.' results' . EOL; + + $str = 'SQL = ' . $sql . EOL . 'SQL returned ' . $mesg . EOL; + + switch($this->debug) { + case 3: + echo $str; + break; + default: + $debug_text .= $str; + break; + } + } + + if(($result === true) || ($result === false)) + return $result; + + $r = array(); + if($result->num_rows) { + while($x = $result->fetch_array(MYSQL_ASSOC)) + $r[] = $x; + $result->free_result(); + } + + if($this->debug == 2) + $debug_text .= print_r($r, true). EOL; +// $debug_text .= quoted_printable_encode(print_r($r, true). EOL); + elseif($this->debug == 3) + echo print_r($r, true) . EOL ; +// echo quoted_printable_encode(print_r($r, true) . EOL) ; + + return($r); + } + + public function dbg($dbg) { + $this->debug = $dbg; + } + + public function escape($str) { + return @$this->db->real_escape_string($str); + } + + function __destruct() { + @$this->db->close(); + } +}} + +// Procedural functions +if(! function_exists('dbg')) { +function dbg($state) { + global $db; + $db->dbg($state); +}} + +if(! function_exists('dbesc')) { +function dbesc($str) { + global $db; + return($db->escape($str)); +}} + + +// Function: q($sql,$args); +// Description: execute SQL query with printf style args. +// Example: $r = q("SELECT * FROM `%s` WHERE `uid` = %d", +// 'user', 1); + +if(! function_exists('q')) { +function q($sql) { + + global $db; + $args = func_get_args(); + unset($args[0]); + $ret = $db->q(vsprintf($sql,$args)); + return $ret; +}} + + +// Caller is responsible for ensuring that any integer arguments to +// dbesc_array are actually integers and not malformed strings containing +// SQL injection vectors. All integer array elements should be specifically +// cast to int to avoid trouble. + + +if(! function_exists('dbesc_array_cb')) { +function dbesc_array_cb(&$item, $key) { + if(is_string($item)) + $item = dbesc($item); +}} + + +if(! function_exists('dbesc_array')) { +function dbesc_array(&$a) { + if(is_array($a) && count($a)) { + array_walk($a,'dbesc_array_cb'); + } +}} \ No newline at end of file diff --git a/include/login.php b/include/login.php new file mode 100644 index 0000000..b11ee17 --- /dev/null +++ b/include/login.php @@ -0,0 +1,19 @@ + +
    + + + + + +
    diff --git a/include/security.php b/include/security.php new file mode 100644 index 0000000..8b34525 --- /dev/null +++ b/include/security.php @@ -0,0 +1,17 @@ + +System Unavailable + +Apologies but this site is unavailable at the moment. Please try again later. + + \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..a2d05d5 --- /dev/null +++ b/index.php @@ -0,0 +1,113 @@ +init_pagehead(); + +session_start(); + +if((x($_SESSION,'authenticated')) || (x($_POST['auth-params']))) + require("auth.php"); + +if($install) + $a->module = 'install'; + +if(strlen($a->module)) { + if(file_exists("mod/{$a->module}.php")) { + include("mod/{$a->module}.php"); + $a->module_loaded = true; + } + else { + // TODO + // search builtin function module table, else + // return 403, 404, etc. Right now unresolved pages return blank. + } +} + +// invoke module functions +// Important: Modules normally do not emit content, unless you need it for debugging. +// The module_init, module_post, and module_afterpost functions process URL parameters and POST processing. +// The module_content function returns content text to this function where it is included on the page. +// Modules emitting XML/Atom, etc. should do so in the _init function and promptly exit. +// "Most" HTML resides in the view directory as text templates with macro substitution. +// They look like HTML with PHP variables but only a couple pass through the PHP processor - those with .php extensions. +// The macro substitution is defined per page for the .tpl files. +// Information transfer between functions can be accomplished via the App session '$a' and its related variables. +// x() queries both a variable's existence and that it is "non-zero" or "non-empty" depending on how it is called. +// q() is the SQL query form. All string (%s) variables MUST be passed through dbesc(). +// All int values MUST be cast to integer using intval(); + +if($a->module_loaded) { + $a->page['page_title'] = $a->module; + if(function_exists($a->module . '_init')) { + $func = $a->module . '_init'; + $func($a); + } + + if(($_SERVER['REQUEST_METHOD'] == 'POST') && (! $a->error) + && (function_exists($a->module . '_post')) + && (! x($_POST,'auth-params'))) { + $func = $a->module . '_post'; + $func($a); + } + + if((! $a->error) && (function_exists($a->module . '_afterpost'))) { + $func = $a->module . '_afterpost'; + $func($a); + } + + if((! $a->error) && (function_exists($a->module . '_content'))) { + $func = $a->module . '_content'; + $a->page['content'] .= $func($a); + } + + footer($a); +} + +// report anything important happening + +if(x($_SESSION,'sysmsg')) { + $a->page['content'] = "
    {$_SESSION['sysmsg']}
    \r\n" + . $a->page['content']; + unset($_SESSION['sysmsg']); +} + +// Feel free to comment out this line on production sites. +$a->page['content'] .= $debug_text; + +// build page + +// Navigation (menu) template +require_once("nav.php"); + +$page = $a->page; +$profile = $a->profile; + +header("Content-type: text/html; charset=utf-8"); +$template = "view/" + . ((x($a->page,'theme')) ? $a->page['theme'] . '/' : "" ) + . ((x($a->page,'template')) ? $a->page['template'] : 'default' ) + . ".php"; + +require_once($template); + +session_write_close(); +exit; diff --git a/library/HTML5/Data.php b/library/HTML5/Data.php new file mode 100644 index 0000000..fa97e3e --- /dev/null +++ b/library/HTML5/Data.php @@ -0,0 +1,120 @@ + 0x000A, // LINE FEED (LF) + 0x80 => 0x20AC, // EURO SIGN ('€') + 0x81 => 0xFFFD, // REPLACEMENT CHARACTER + 0x82 => 0x201A, // SINGLE LOW-9 QUOTATION MARK ('‚') + 0x83 => 0x0192, // LATIN SMALL LETTER F WITH HOOK ('Æ’') + 0x84 => 0x201E, // DOUBLE LOW-9 QUOTATION MARK ('„') + 0x85 => 0x2026, // HORIZONTAL ELLIPSIS ('…') + 0x86 => 0x2020, // DAGGER ('†') + 0x87 => 0x2021, // DOUBLE DAGGER ('‡') + 0x88 => 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT ('ˆ') + 0x89 => 0x2030, // PER MILLE SIGN ('‰') + 0x8A => 0x0160, // LATIN CAPITAL LETTER S WITH CARON ('Å ') + 0x8B => 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK ('‹') + 0x8C => 0x0152, // LATIN CAPITAL LIGATURE OE ('Å’') + 0x8D => 0xFFFD, // REPLACEMENT CHARACTER + 0x8E => 0x017D, // LATIN CAPITAL LETTER Z WITH CARON ('Ž') + 0x8F => 0xFFFD, // REPLACEMENT CHARACTER + 0x90 => 0xFFFD, // REPLACEMENT CHARACTER + 0x91 => 0x2018, // LEFT SINGLE QUOTATION MARK ('‘') + 0x92 => 0x2019, // RIGHT SINGLE QUOTATION MARK ('’') + 0x93 => 0x201C, // LEFT DOUBLE QUOTATION MARK ('“') + 0x94 => 0x201D, // RIGHT DOUBLE QUOTATION MARK ('â€') + 0x95 => 0x2022, // BULLET ('•') + 0x96 => 0x2013, // EN DASH ('–') + 0x97 => 0x2014, // EM DASH ('—') + 0x98 => 0x02DC, // SMALL TILDE ('Ëœ') + 0x99 => 0x2122, // TRADE MARK SIGN ('â„¢') + 0x9A => 0x0161, // LATIN SMALL LETTER S WITH CARON ('Å¡') + 0x9B => 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK ('›') + 0x9C => 0x0153, // LATIN SMALL LIGATURE OE ('Å“') + 0x9D => 0xFFFD, // REPLACEMENT CHARACTER + 0x9E => 0x017E, // LATIN SMALL LETTER Z WITH CARON ('ž') + 0x9F => 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS ('Ÿ') + ); + + protected static $namedCharacterReferences; + + protected static $namedCharacterReferenceMaxLength; + + /** + * Returns the "real" Unicode codepoint of a malformed character + * reference. + */ + public static function getRealCodepoint($ref) { + if (!isset(self::$realCodepointTable[$ref])) return false; + else return self::$realCodepointTable[$ref]; + } + + public static function getNamedCharacterReferences() { + if (!self::$namedCharacterReferences) { + self::$namedCharacterReferences = unserialize( + file_get_contents(dirname(__FILE__) . '/named-character-references.ser')); + } + return self::$namedCharacterReferences; + } + + public static function getNamedCharacterReferenceMaxLength() { + if (!self::$namedCharacterReferenceMaxLength) { + $namedCharacterReferences = self::getNamedCharacterReferences(); + $lengths = array_map('strlen', array_keys($namedCharacterReferences)); + self::$namedCharacterReferenceMaxLength = max($lengths); + } + return self::$namedCharacterReferenceMaxLength; + } + + + /** + * Converts a Unicode codepoint to sequence of UTF-8 bytes. + * @note Shamelessly stolen from HTML Purifier, which is also + * shamelessly stolen from Feyd (which is in public domain). + */ + public static function utf8chr($code) { + if($code > 0x10FFFF or $code < 0x0 or + ($code >= 0xD800 and $code <= 0xDFFF) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return "\xEF\xBF\xBD"; + } + + $x = $y = $z = $w = 0; + if ($code < 0x80) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 0x3F) | 0x80; + if ($code < 0x800) { + $y = (($code & 0x7FF) >> 6) | 0xC0; + } else { + $y = (($code & 0xFC0) >> 6) | 0x80; + if($code < 0x10000) { + $z = (($code >> 12) & 0x0F) | 0xE0; + } else { + $z = (($code >> 12) & 0x3F) | 0x80; + $w = (($code >> 18) & 0x07) | 0xF0; + } + } + } + // set up the actual character + $ret = ''; + if($w) $ret .= chr($w); + if($z) $ret .= chr($z); + if($y) $ret .= chr($y); + $ret .= chr($x); + + return $ret; + } + +} diff --git a/library/HTML5/InputStream.php b/library/HTML5/InputStream.php new file mode 100644 index 0000000..f98b427 --- /dev/null +++ b/library/HTML5/InputStream.php @@ -0,0 +1,284 @@ + + +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. + +*/ + +// Some conventions: +// /* */ indicates verbatim text from the HTML 5 specification +// // indicates regular comments + +class HTML5_InputStream { + /** + * The string data we're parsing. + */ + private $data; + + /** + * The current integer byte position we are in $data + */ + private $char; + + /** + * Length of $data; when $char === $data, we are at the end-of-file. + */ + private $EOF; + + /** + * Parse errors. + */ + public $errors = array(); + + /** + * @param $data Data to parse + */ + public function __construct($data) { + + /* Given an encoding, the bytes in the input stream must be + converted to Unicode characters for the tokeniser, as + described by the rules for that encoding, except that the + leading U+FEFF BYTE ORDER MARK character, if any, must not + be stripped by the encoding layer (it is stripped by the rule below). + + Bytes or sequences of bytes in the original byte stream that + could not be converted to Unicode characters must be converted + to U+FFFD REPLACEMENT CHARACTER code points. */ + + // XXX currently assuming input data is UTF-8; once we + // build encoding detection this will no longer be the case + // + // We previously had an mbstring implementation here, but that + // implementation is heavily non-conforming, so it's been + // omitted. + if (extension_loaded('iconv')) { + // non-conforming + $data = @iconv('UTF-8', 'UTF-8//IGNORE', $data); + } else { + // we can make a conforming native implementation + throw new Exception('Not implemented, please install mbstring or iconv'); + } + + /* One leading U+FEFF BYTE ORDER MARK character must be + ignored if any are present. */ + if (substr($data, 0, 3) === "\xEF\xBB\xBF") { + $data = substr($data, 3); + } + + /* All U+0000 NULL characters in the input must be replaced + by U+FFFD REPLACEMENT CHARACTERs. Any occurrences of such + characters is a parse error. */ + for ($i = 0, $count = substr_count($data, "\0"); $i < $count; $i++) { + $this->errors[] = array( + 'type' => HTML5_Tokenizer::PARSEERROR, + 'data' => 'null-character' + ); + } + /* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED + (LF) characters are treated specially. Any CR characters + that are followed by LF characters must be removed, and any + CR characters not followed by LF characters must be converted + to LF characters. Thus, newlines in HTML DOMs are represented + by LF characters, and there are never any CR characters in the + input to the tokenization stage. */ + $data = str_replace( + array( + "\0", + "\r\n", + "\r" + ), + array( + "\xEF\xBF\xBD", + "\n", + "\n" + ), + $data + ); + + /* Any occurrences of any characters in the ranges U+0001 to + U+0008, U+000B, U+000E to U+001F, U+007F to U+009F, + U+D800 to U+DFFF , U+FDD0 to U+FDEF, and + characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, + U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, + U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, + U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE, + U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and + U+10FFFF are parse errors. (These are all control characters + or permanently undefined Unicode characters.) */ + // Check PCRE is loaded. + if (extension_loaded('pcre')) { + $count = preg_match_all( + '/(?: + [\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F + | + \xC2[\x80-\x9F] # U+0080 to U+009F + | + \xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF + | + \xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF + | + \xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF + | + [\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16}) + )/x', + $data, + $matches + ); + for ($i = 0; $i < $count; $i++) { + $this->errors[] = array( + 'type' => HTML5_Tokenizer::PARSEERROR, + 'data' => 'invalid-codepoint' + ); + } + } else { + // XXX: Need non-PCRE impl, probably using substr_count + } + + $this->data = $data; + $this->char = 0; + $this->EOF = strlen($data); + } + + /** + * Returns the current line that the tokenizer is at. + */ + public function getCurrentLine() { + // Check the string isn't empty + if($this->EOF) { + // Add one to $this->char because we want the number for the next + // byte to be processed. + return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1; + } else { + // If the string is empty, we are on the first line (sorta). + return 1; + } + } + + /** + * Returns the current column of the current line that the tokenizer is at. + */ + public function getColumnOffset() { + // strrpos is weird, and the offset needs to be negative for what we + // want (i.e., the last \n before $this->char). This needs to not have + // one (to make it point to the next character, the one we want the + // position of) added to it because strrpos's behaviour includes the + // final offset byte. + $lastLine = strrpos($this->data, "\n", $this->char - 1 - strlen($this->data)); + + // However, for here we want the length up until the next byte to be + // processed, so add one to the current byte ($this->char). + if($lastLine !== false) { + $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine); + } else { + $findLengthOf = substr($this->data, 0, $this->char); + } + + // Get the length for the string we need. + if(extension_loaded('iconv')) { + return iconv_strlen($findLengthOf, 'utf-8'); + } elseif(extension_loaded('mbstring')) { + return mb_strlen($findLengthOf, 'utf-8'); + } elseif(extension_loaded('xml')) { + return strlen(utf8_decode($findLengthOf)); + } else { + $count = count_chars($findLengthOf); + // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range) + // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range) + return array_sum(array_slice($count, 0, 0x80)) + + array_sum(array_slice($count, 0xC2, 0x33)); + } + } + + /** + * Retrieve the currently consume character. + * @note This performs bounds checking + */ + public function char() { + return ($this->char++ < $this->EOF) + ? $this->data[$this->char - 1] + : false; + } + + /** + * Get all characters until EOF. + * @note This performs bounds checking + */ + public function remainingChars() { + if($this->char < $this->EOF) { + $data = substr($this->data, $this->char); + $this->char = $this->EOF; + return $data; + } else { + return false; + } + } + + /** + * Matches as far as possible until we reach a certain set of bytes + * and returns the matched substring. + * @param $bytes Bytes to match. + */ + public function charsUntil($bytes, $max = null) { + if ($this->char < $this->EOF) { + if ($max === 0 || $max) { + $len = strcspn($this->data, $bytes, $this->char, $max); + } else { + $len = strcspn($this->data, $bytes, $this->char); + } + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + return $string; + } else { + return false; + } + } + + /** + * Matches as far as possible with a certain set of bytes + * and returns the matched substring. + * @param $bytes Bytes to match. + */ + public function charsWhile($bytes, $max = null) { + if ($this->char < $this->EOF) { + if ($max === 0 || $max) { + $len = strspn($this->data, $bytes, $this->char, $max); + } else { + $len = strspn($this->data, $bytes, $this->char); + } + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + return $string; + } else { + return false; + } + } + + /** + * Unconsume one character. + */ + public function unget() { + if ($this->char <= $this->EOF) { + $this->char--; + } + } +} diff --git a/library/HTML5/Parser.php b/library/HTML5/Parser.php new file mode 100644 index 0000000..5f9ca56 --- /dev/null +++ b/library/HTML5/Parser.php @@ -0,0 +1,36 @@ +parse(); + return $tokenizer->save(); + } + /** + * Parses an HTML fragment. + * @param $text HTML text to parse + * @param $context String name of context element to pretend parsing is in. + * @param $builder Custom builder implementation + * @return Parsed HTML as DOMDocument + */ + static public function parseFragment($text, $context = null, $builder = null) { + $tokenizer = new HTML5_Tokenizer($text, $builder); + $tokenizer->parseFragment($context); + return $tokenizer->save(); + } +} diff --git a/library/HTML5/Tokenizer.php b/library/HTML5/Tokenizer.php new file mode 100644 index 0000000..06c7306 --- /dev/null +++ b/library/HTML5/Tokenizer.php @@ -0,0 +1,2307 @@ + +Copyright 2008 Edward Z. Yang +Copyright 2009 Geoffrey Sneddon + +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. + +*/ + +// Some conventions: +// /* */ indicates verbatim text from the HTML 5 specification +// // indicates regular comments + +// all flags are in hyphenated form + +class HTML5_Tokenizer { + /** + * Points to an InputStream object. + */ + protected $stream; + + /** + * Tree builder that the tokenizer emits token to. + */ + private $tree; + + /** + * Current content model we are parsing as. + */ + protected $content_model; + + /** + * Current token that is being built, but not yet emitted. Also + * is the last token emitted, if applicable. + */ + protected $token; + + // These are constants describing the content model + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + // These are constants describing tokens + // XXX should probably be moved somewhere else, probably the + // HTML5 class. + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTER = 4; + const SPACECHARACTER = 5; + const EOF = 6; + const PARSEERROR = 7; + + // These are constants representing bunches of characters. + const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz'; + const DIGIT = '0123456789'; + const HEX = '0123456789ABCDEFabcdef'; + const WHITESPACE = "\t\n\x0c "; + + /** + * @param $data Data to parse + */ + public function __construct($data, $builder = null) { + $this->stream = new HTML5_InputStream($data); + if (!$builder) $this->tree = new HTML5_TreeBuilder; + $this->content_model = self::PCDATA; + } + + public function parseFragment($context = null) { + $this->tree->setupContext($context); + if ($this->tree->content_model) { + $this->content_model = $this->tree->content_model; + $this->tree->content_model = null; + } + $this->parse(); + } + + // XXX maybe convert this into an iterator? regardless, this function + // and the save function should go into a Parser facade of some sort + /** + * Performs the actual parsing of the document. + */ + public function parse() { + // Current state + $state = 'data'; + // This is used to avoid having to have look-behind in the data state. + $lastFourChars = ''; + /** + * Escape flag as specified by the HTML5 specification: "used to + * control the behavior of the tokeniser. It is either true or + * false, and initially must be set to the false state." + */ + $escape = false; + //echo "\n\n"; + while($state !== null) { + + /*echo $state . ' '; + switch ($this->content_model) { + case self::PCDATA: echo 'PCDATA'; break; + case self::RCDATA: echo 'RCDATA'; break; + case self::CDATA: echo 'CDATA'; break; + case self::PLAINTEXT: echo 'PLAINTEXT'; break; + } + if ($escape) echo " escape"; + echo "\n";*/ + + switch($state) { + case 'data': + + /* Consume the next input character */ + $char = $this->stream->char(); + $lastFourChars .= $char; + if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4); + + // see below for meaning + $hyp_cond = + !$escape && + ( + $this->content_model === self::RCDATA || + $this->content_model === self::CDATA + ); + $amp_cond = + !$escape && + ( + $this->content_model === self::PCDATA || + $this->content_model === self::RCDATA + ); + $lt_cond = + $this->content_model === self::PCDATA || + ( + ( + $this->content_model === self::RCDATA || + $this->content_model === self::CDATA + ) && + !$escape + ); + $gt_cond = + $escape && + ( + $this->content_model === self::RCDATA || + $this->content_model === self::CDATA + ); + + if($char === '&' && $amp_cond) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states and the escape flag is false: switch to the + character reference data state. Otherwise: treat it as per + the "anything else" entry below. */ + $state = 'characterReferenceData'; + + } elseif( + $char === '-' && + $hyp_cond && + $lastFourChars === '' + ) { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is true, and the last three + characters in the input stream including this one are U+002D + HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"), + set the escape flag to false. */ + $escape = false; + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => '>' + )); + // We do the "any case" part as part of "anything else". + + } elseif($char === false) { + /* EOF + Emit an end-of-file token. */ + $state = null; + $this->tree->emitToken(array( + 'type' => self::EOF + )); + + } elseif($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + // Directly after emitting a token you switch back to the "data + // state". At that point spaceCharacters are important so they are + // emitted separately. + $chars = $this->stream->charsWhile(self::WHITESPACE); + $this->emitToken(array( + 'type' => self::SPACECHARACTER, + 'data' => $char . $chars + )); + $lastFourChars .= $chars; + if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4); + + } else { + /* Anything else + THIS IS AN OPTIMIZATION: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + + $mask = ''; + if ($hyp_cond) $mask .= '-'; + if ($amp_cond) $mask .= '&'; + if ($lt_cond) $mask .= '<'; + if ($gt_cond) $mask .= '>'; + + if ($mask === '') { + $chars = $this->stream->remainingChars(); + } else { + $chars = $this->stream->charsUntil($mask); + } + + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => $char . $chars + )); + + $lastFourChars .= $chars; + if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4); + + $state = 'data'; + } + break; + + case 'characterReferenceData': + /* (This cannot happen if the content model flag + is set to the CDATA state.) */ + + /* Attempt to consume a character reference, with no + additional allowed character. */ + $entity = $this->consumeCharacterReference(); + + /* If nothing is returned, emit a U+0026 AMPERSAND + character token. Otherwise, emit the character token that + was returned. */ + // This is all done when consuming the character reference. + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => $entity + )); + + /* Finally, switch to the data state. */ + $state = 'data'; + break; + + case 'tagOpen': + $char = $this->stream->char(); + + switch($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* Consume the next input character. If it is a + U+002F SOLIDUS (/) character, switch to the close + tag open state. Otherwise, emit a U+003C LESS-THAN + SIGN character token and reconsume the current input + character in the data state. */ + // We consumed above. + + if($char === '/') { + $state = 'closeTagOpen'; + + } else { + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => '<' + )); + + $this->stream->unget(); + + $state = 'data'; + } + break; + + case self::PCDATA: + /* If the content model flag is set to the PCDATA state + Consume the next input character: */ + // We consumed above. + + if($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $state = 'markupDeclarationOpen'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $state = 'closeTagOpen'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $state = 'tagName'; + + } elseif('a' <= $char && $char <= 'z') { + /* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z + Create a new start tag token, set its tag name to the input + character, then switch to the tag name state. (Don't emit + the token yet; further details will be filled in before it + is emitted.) */ + $this->token = array( + 'name' => $char, + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-tag-name-but-got-right-bracket' + )); + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => '<>' + )); + + $state = 'data'; + + } elseif($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-tag-name-but-got-question-mark' + )); + $this->token = array( + 'data' => '?', + 'type' => self::COMMENT + ); + $state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-tag-name' + )); + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => '<' + )); + + $state = 'data'; + $this->stream->unget(); + } + break; + } + break; + + case 'closeTagOpen': + if ( + $this->content_model === self::RCDATA || + $this->content_model === self::CDATA + ) { + /* If the content model flag is set to the RCDATA or CDATA + states... */ + $name = strtolower($this->stream->charsWhile(self::ALPHA)); + $following = $this->stream->char(); + $this->stream->unget(); + if ( + !$this->token || + $this->token['name'] !== $name || + $this->token['name'] === $name && !in_array($following, array("\x09", "\x0A", "\x0C", "\x20", "\x3E", "\x2F", false)) + ) { + /* if no start tag token has ever been emitted by this instance + of the tokenizer (fragment case), or, if the next few + characters do not match the tag name of the last start tag + token emitted (compared in an ASCII case-insensitive manner), + or if they do but they are not immediately followed by one of + the following characters: + + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + + ...then emit a U+003C LESS-THAN SIGN character token, a + U+002F SOLIDUS character token, and switch to the data + state to process the next input character. */ + // XXX: Probably ought to replace in_array with $following === x ||... + + // We also need to emit $name now we've consumed that, as we + // know it'll just be emitted as a character token. + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => 'token = array( + 'name' => $name, + 'type' => self::ENDTAG + ); + + // Change to tag name state. + $state = 'tagName'; + } + } elseif ($this->content_model === self::PCDATA) { + /* Otherwise, if the content model flag is set to the PCDATA + state [...]: */ + $char = $this->stream->char(); + + if ('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $state = 'tagName'; + + } elseif ('a' <= $char && $char <= 'z') { + /* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z + Create a new end tag token, set its tag name to the + input character, then switch to the tag name state. + (Don't emit the token yet; further details will be + filled in before it is emitted.) */ + $this->token = array( + 'name' => $char, + 'type' => self::ENDTAG + ); + + $state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-closing-tag-but-got-right-bracket' + )); + $state = 'data'; + + } elseif($char === false) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-closing-tag-but-got-eof' + )); + $this->emitToken(array( + 'type' => self::CHARACTER, + 'data' => 'stream->unget(); + $state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-closing-tag-but-got-char' + )); + $this->token = array( + 'data' => $char, + 'type' => self::COMMENT + ); + $state = 'bogusComment'; + } + } + break; + + case 'tagName': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $state = 'beforeAttributeName'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the self-closing start tag state. */ + $state = 'selfClosingStartTag'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Append the lowercase version of the current input + character (add 0x0020 to the character's code point) to + the current tag token's tag name. Stay in the tag name state. */ + $chars = $this->stream->charsWhile(self::UPPER_ALPHA); + + $this->token['name'] .= strtolower($char . $chars); + $state = 'tagName'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-tag-name' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $chars = $this->stream->charsUntil("\t\n\x0C />" . self::UPPER_ALPHA); + + $this->token['name'] .= $char . $chars; + $state = 'tagName'; + } + break; + + case 'beforeAttributeName': + /* Consume the next input character: */ + $char = $this->stream->char(); + + // this conditional is optimized, check bottom + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $state = 'beforeAttributeName'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the self-closing start tag state. */ + $state = 'selfClosingStartTag'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Start a new attribute in the current tag token. Set that + attribute's name to the lowercase version of the current + input character (add 0x0020 to the character's code + point), and its value to the empty string. Switch to the + attribute name state.*/ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => '' + ); + + $state = 'attributeName'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-attribute-name-but-got-eof' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* U+0022 QUOTATION MARK (") + U+0027 APOSTROPHE (') + U+003D EQUALS SIGN (=) + Parse error. Treat it as per the "anything else" entry + below. */ + if($char === '"' || $char === "'" || $char === '=') { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'invalid-character-in-attribute-name' + )); + } + + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => $char, + 'value' => '' + ); + + $state = 'attributeName'; + } + break; + + case 'attributeName': + // Consume the next input character: + $char = $this->stream->char(); + + // this conditional is optimized, check bottom + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the after attribute name state. */ + $state = 'afterAttributeName'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the self-closing start tag state. */ + $state = 'selfClosingStartTag'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Append the lowercase version of the current input + character (add 0x0020 to the character's code point) to + the current attribute's name. Stay in the attribute name + state. */ + $chars = $this->stream->charsWhile(self::UPPER_ALPHA); + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char . $chars); + + $state = 'attributeName'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-attribute-name' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* U+0022 QUOTATION MARK (") + U+0027 APOSTROPHE (') + Parse error. Treat it as per the "anything else" + entry below. */ + if($char === '"' || $char === "'") { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'invalid-character-in-attribute-name' + )); + } + + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $chars = $this->stream->charsUntil("\t\n\x0C /=>\"'" . self::UPPER_ALPHA); + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= $char . $chars; + + $state = 'attributeName'; + } + + /* When the user agent leaves the attribute name state + (and before emitting the tag token, if appropriate), the + complete attribute's name must be compared to the other + attributes on the same token; if there is already an + attribute on the token with the exact same name, then this + is a parse error and the new attribute must be dropped, along + with the value that gets associated with it (if any). */ + // this might be implemented in the emitToken method + break; + + case 'afterAttributeName': + // Consume the next input character: + $char = $this->stream->char(); + + // this is an optimized conditional, check the bottom + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $state = 'afterAttributeName'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the self-closing start tag state. */ + $state = 'selfClosingStartTag'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Start a new attribute in the current tag token. Set that + attribute's name to the lowercase version of the current + input character (add 0x0020 to the character's code + point), and its value to the empty string. Switch to the + attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => '' + ); + + $state = 'attributeName'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-end-of-tag-but-got-eof' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* U+0022 QUOTATION MARK (") + U+0027 APOSTROPHE (') + Parse error. Treat it as per the "anything else" + entry below. */ + if($char === '"' || $char === "'") { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'invalid-character-after-attribute-name' + )); + } + + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => $char, + 'value' => '' + ); + + $state = 'attributeName'; + } + break; + + case 'beforeAttributeValue': + // Consume the next input character: + $char = $this->stream->char(); + + // this is an optimized conditional + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $state = 'beforeAttributeValue'; + + } elseif($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $state = 'attributeValueDoubleQuoted'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->stream->unget(); + $state = 'attributeValueUnquoted'; + + } elseif($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $state = 'attributeValueSingleQuoted'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit the current tag token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-attribute-value-but-got-right-bracket' + )); + $this->emitToken($this->token); + $state = 'data'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume + the character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-attribute-value-but-got-eof' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* U+003D EQUALS SIGN (=) + Parse error. Treat it as per the "anything else" entry below. */ + if($char === '=') { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'equals-in-unquoted-attribute-value' + )); + } + + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $state = 'attributeValueUnquoted'; + } + break; + + case 'attributeValueDoubleQuoted': + // Consume the next input character: + $char = $this->stream->char(); + + if($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the after attribute value (quoted) state. */ + $state = 'afterAttributeValueQuoted'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the character reference in attribute value + state, with the additional allowed character + being U+0022 QUOTATION MARK ("). */ + $this->characterReferenceInAttributeValue('"'); + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-attribute-value-double-quote' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $chars = $this->stream->charsUntil('"&'); + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char . $chars; + + $state = 'attributeValueDoubleQuoted'; + } + break; + + case 'attributeValueSingleQuoted': + // Consume the next input character: + $char = $this->stream->char(); + + if($char === "'") { + /* U+0022 QUOTATION MARK (') + Switch to the after attribute value state. */ + $state = 'afterAttributeValueQuoted'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->characterReferenceInAttributeValue("'"); + + } elseif($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-attribute-value-single-quote' + )); + $this->emitToken($this->token); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $chars = $this->stream->charsUntil("'&"); + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char . $chars; + + $state = 'attributeValueSingleQuoted'; + } + break; + + case 'attributeValueUnquoted': + // Consume the next input character: + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->characterReferenceInAttributeValue(); + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif ($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume + the character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-attribute-value-no-quotes' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* U+0022 QUOTATION MARK (") + U+0027 APOSTROPHE (') + U+003D EQUALS SIGN (=) + Parse error. Treat it as per the "anything else" + entry below. */ + if($char === '"' || $char === "'" || $char === '=') { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-character-in-unquoted-attribute-value' + )); + } + + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $chars = $this->stream->charsUntil("\t\n\x0c &>\"'="); + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char . $chars; + + $state = 'attributeValueUnquoted'; + } + break; + + case 'afterAttributeValueQuoted': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $state = 'beforeAttributeName'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the self-closing start tag state. */ + $state = 'selfClosingStartTag'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif ($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-EOF-after-attribute-value' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Parse error. Reconsume the character in the before attribute + name state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-character-after-attribute-value' + )); + $this->stream->unget(); + $state = 'beforeAttributeName'; + } + break; + + case 'selfClosingStartTag': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Set the self-closing flag of the current tag token. + Emit the current tag token. Switch to the data state. */ + // not sure if this is the name we want + $this->token['self-closing'] = true; + /* When an end tag token is emitted with its self-closing flag set, + that is a parse error. */ + if ($this->token['type'] === self::ENDTAG) { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'self-closing-end-tag' + )); + } + $this->emitToken($this->token); + $state = 'data'; + + } elseif ($char === false) { + /* EOF + Parse error. Emit the current tag token. Reconsume the + EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-eof-after-self-closing' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Parse error. Reconsume the character in the before attribute name state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-character-after-self-closing' + )); + $this->stream->unget(); + $state = 'beforeAttributeName'; + } + break; + + case 'bogusComment': + /* (This can only happen if the content model flag is set to the PCDATA state.) */ + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $this->token['data'] .= (string) $this->stream->charsUntil('>'); + $this->stream->char(); + + $this->emitToken($this->token); + + /* Switch to the data state. */ + $state = 'data'; + break; + + case 'markupDeclarationOpen': + // Consume for below + $hyphens = $this->stream->charsWhile('-', 2); + if ($hyphens === '-') { + $this->stream->unget(); + } + if ($hyphens !== '--') { + $alpha = $this->stream->charsWhile(self::ALPHA, 7); + } + + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if($hyphens === '--') { + $state = 'commentStart'; + $this->token = array( + 'data' => '', + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven characters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif(strtoupper($alpha) === 'DOCTYPE') { + $state = 'doctype'; + + // XXX not implemented + /* Otherwise, if the insertion mode is "in foreign content" + and the current node is not an element in the HTML namespace + and the next seven characters are an ASCII case-sensitive + match for the string "[CDATA[" (the five uppercase letters + "CDATA" with a U+005B LEFT SQUARE BRACKET character before + and after), then consume those characters and switch to the + CDATA section state (which is unrelated to the content model + flag's CDATA state). */ + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-dashes-or-doctype' + )); + $this->token = array( + 'data' => (string) $alpha, + 'type' => self::COMMENT + ); + $state = 'bogusComment'; + } + break; + + case 'commentStart': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === '-') { + /* U+002D HYPHEN-MINUS (-) + Switch to the comment start dash state. */ + $state = 'commentStartDash'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit the comment token. Switch to the + data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'incorrect-comment' + )); + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* EOF + Parse error. Emit the comment token. Reconsume the + EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-comment' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Append the input character to the comment token's + data. Switch to the comment state. */ + $this->token['data'] .= $char; + $state = 'comment'; + } + break; + + case 'commentStartDash': + /* Consume the next input character: */ + $char = $this->stream->char(); + if ($char === '-') { + /* U+002D HYPHEN-MINUS (-) + Switch to the comment end state */ + $state = 'commentEnd'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit the comment token. Switch to the + data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'incorrect-comment' + )); + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* Parse error. Emit the comment token. Reconsume the + EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-comment' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + $this->token['data'] .= '-' . $char; + $state = 'comment'; + } + break; + + case 'comment': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === '-') { + /* U+002D HYPHEN-MINUS (-) + Switch to the comment end dash state */ + $state = 'commentEndDash'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-comment' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append the input character to the comment token's data. Stay in + the comment state. */ + $chars = $this->stream->charsUntil('-'); + + $this->token['data'] .= $char . $chars; + } + break; + + case 'commentEndDash': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === '-') { + /* U+002D HYPHEN-MINUS (-) + Switch to the comment end state */ + $state = 'commentEnd'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-comment-end-dash' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-'.$char; + $state = 'comment'; + } + break; + + case 'commentEnd': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the comment token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif($char === '-') { + /* U+002D HYPHEN-MINUS (-) + Parse error. Append a U+002D HYPHEN-MINUS (-) character + to the comment token's data. Stay in the comment end + state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-dash-after-double-dash-in-comment' + )); + $this->token['data'] .= '-'; + + } elseif($char === false) { + /* EOF + Parse error. Emit the comment token. Reconsume the + EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-comment-double-dash' + )); + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Parse error. Append two U+002D HYPHEN-MINUS (-) + characters and the input character to the comment token's + data. Switch to the comment state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-comment' + )); + $this->token['data'] .= '--'.$char; + $state = 'comment'; + } + break; + + case 'doctype': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before DOCTYPE name state. */ + $state = 'beforeDoctypeName'; + + } else { + /* Anything else + Parse error. Reconsume the current character in the + before DOCTYPE name state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'need-space-after-doctype' + )); + $this->stream->unget(); + $state = 'beforeDoctypeName'; + } + break; + + case 'beforeDoctypeName': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before DOCTYPE name state. */ + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Create a new DOCTYPE token. Set its + force-quirks flag to on. Emit the token. Switch to the + data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-doctype-name-but-got-right-bracket' + )); + $this->emitToken(array( + 'name' => '', + 'type' => self::DOCTYPE, + 'force-quirks' => true, + 'error' => true + )); + + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Create a new DOCTYPE token. Set the token's name to the + lowercase version of the input character (add 0x0020 to + the character's code point). Switch to the DOCTYPE name + state. */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $state = 'doctypeName'; + + } elseif($char === false) { + /* EOF + Parse error. Create a new DOCTYPE token. Set its + force-quirks flag to on. Emit the token. Reconsume the + EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-doctype-name-but-got-eof' + )); + $this->emitToken(array( + 'name' => '', + 'type' => self::DOCTYPE, + 'force-quirks' => true, + 'error' => true + )); + + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Create a new DOCTYPE token. Set the token's name to the + current input character. Switch to the DOCTYPE name state. */ + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $state = 'doctypeName'; + } + break; + + case 'doctypeName': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the after DOCTYPE name state. */ + $state = 'afterDoctypeName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current DOCTYPE token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif('A' <= $char && $char <= 'Z') { + /* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z + Append the lowercase version of the input character + (add 0x0020 to the character's code point) to the current + DOCTYPE token's name. Stay in the DOCTYPE name state. */ + $this->token['name'] .= strtolower($char); + + } elseif($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype-name' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Append the current input character to the current + DOCTYPE token's name. Stay in the DOCTYPE name state. */ + $this->token['name'] .= $char; + } + + // XXX this is probably some sort of quirks mode designation, + // check tree-builder to be sure. In general 'error' needs + // to be specc'ified, this probably means removing it at the end + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + break; + + case 'afterDoctypeName': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after DOCTYPE name state. */ + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current DOCTYPE token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else */ + + $nextSix = strtoupper($char . $this->stream->charsWhile(self::ALPHA, 5)); + if ($nextSix === 'PUBLIC') { + /* If the next six characters are an ASCII + case-insensitive match for the word "PUBLIC", then + consume those characters and switch to the before + DOCTYPE public identifier state. */ + $state = 'beforeDoctypePublicIdentifier'; + + } elseif ($nextSix === 'SYSTEM') { + /* Otherwise, if the next six characters are an ASCII + case-insensitive match for the word "SYSTEM", then + consume those characters and switch to the before + DOCTYPE system identifier state. */ + $state = 'beforeDoctypeSystemIdentifier'; + + } else { + /* Otherwise, this is the parse error. Set the DOCTYPE + token's force-quirks flag to on. Switch to the bogus + DOCTYPE state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-space-or-right-bracket-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->token['error'] = true; + $state = 'bogusDoctype'; + } + } + break; + + case 'beforeDoctypePublicIdentifier': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before DOCTYPE public identifier state. */ + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Set the DOCTYPE token's public identifier to the empty + string (not missing), then switch to the DOCTYPE public + identifier (double-quoted) state. */ + $this->token['public'] = ''; + $state = 'doctypePublicIdentifierDoubleQuoted'; + } elseif ($char === "'") { + /* U+0027 APOSTROPHE (') + Set the DOCTYPE token's public identifier to the empty + string (not missing), then switch to the DOCTYPE public + identifier (single-quoted) state. */ + $this->token['public'] = ''; + $state = 'doctypePublicIdentifierSingleQuoted'; + } elseif ($char === '>') { + /* Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-end-of-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* Parse error. Set the DOCTYPE token's force-quirks + flag to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Parse error. Set the DOCTYPE token's force-quirks flag + to on. Switch to the bogus DOCTYPE state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-doctype' + )); + $this->token['force-quirks'] = true; + $state = 'bogusDoctype'; + } + break; + + case 'doctypePublicIdentifierDoubleQuoted': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the after DOCTYPE public identifier state. */ + $state = 'afterDoctypePublicIdentifier'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-end-of-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Append the current input character to the current + DOCTYPE token's public identifier. Stay in the DOCTYPE + public identifier (double-quoted) state. */ + $this->token['public'] .= $char; + } + break; + + case 'doctypePublicIdentifierSingleQuoted': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === "'") { + /* U+0027 APOSTROPHE (') + Switch to the after DOCTYPE public identifier state. */ + $state = 'afterDoctypePublicIdentifier'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-end-of-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Append the current input character to the current + DOCTYPE token's public identifier. Stay in the DOCTYPE + public identifier (double-quoted) state. */ + $this->token['public'] .= $char; + } + break; + + case 'afterDoctypePublicIdentifier': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after DOCTYPE public identifier state. */ + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Set the DOCTYPE token's system identifier to the + empty string (not missing), then switch to the DOCTYPE + system identifier (double-quoted) state. */ + $this->token['system'] = ''; + $state = 'doctypeSystemIdentifierDoubleQuoted'; + } elseif ($char === "'") { + /* U+0027 APOSTROPHE (') + Set the DOCTYPE token's system identifier to the + empty string (not missing), then switch to the DOCTYPE + system identifier (single-quoted) state. */ + $this->token['system'] = ''; + $state = 'doctypeSystemIdentifierSingleQuoted'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current DOCTYPE token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* Parse error. Set the DOCTYPE token's force-quirks + flag to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Switch to the bogus DOCTYPE state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-doctype' + )); + $this->token['force-quirks'] = true; + $state = 'bogusDoctype'; + } + break; + + case 'beforeDoctypeSystemIdentifier': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before DOCTYPE system identifier state. */ + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Set the DOCTYPE token's system identifier to the empty + string (not missing), then switch to the DOCTYPE system + identifier (double-quoted) state. */ + $this->token['system'] = ''; + $state = 'doctypeSystemIdentifierDoubleQuoted'; + } elseif ($char === "'") { + /* U+0027 APOSTROPHE (') + Set the DOCTYPE token's system identifier to the empty + string (not missing), then switch to the DOCTYPE system + identifier (single-quoted) state. */ + $this->token['system'] = ''; + $state = 'doctypeSystemIdentifierSingleQuoted'; + } elseif ($char === '>') { + /* Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* Parse error. Set the DOCTYPE token's force-quirks + flag to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Parse error. Set the DOCTYPE token's force-quirks flag + to on. Switch to the bogus DOCTYPE state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-doctype' + )); + $this->token['force-quirks'] = true; + $state = 'bogusDoctype'; + } + break; + + case 'doctypeSystemIdentifierDoubleQuoted': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the after DOCTYPE system identifier state. */ + $state = 'afterDoctypeSystemIdentifier'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-end-of-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Append the current input character to the current + DOCTYPE token's system identifier. Stay in the DOCTYPE + system identifier (double-quoted) state. */ + $this->token['system'] .= $char; + } + break; + + case 'doctypeSystemIdentifierSingleQuoted': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === "'") { + /* U+0027 APOSTROPHE (') + Switch to the after DOCTYPE system identifier state. */ + $state = 'afterDoctypeSystemIdentifier'; + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-end-of-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* EOF + Parse error. Set the DOCTYPE token's force-quirks flag + to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Append the current input character to the current + DOCTYPE token's system identifier. Stay in the DOCTYPE + system identifier (double-quoted) state. */ + $this->token['system'] .= $char; + } + break; + + case 'afterDoctypeSystemIdentifier': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after DOCTYPE system identifier state. */ + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current DOCTYPE token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + } elseif ($char === false) { + /* Parse error. Set the DOCTYPE token's force-quirks + flag to on. Emit that DOCTYPE token. Reconsume the EOF + character in the data state. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'eof-in-doctype' + )); + $this->token['force-quirks'] = true; + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + } else { + /* Anything else + Parse error. Switch to the bogus DOCTYPE state. + (This does not set the DOCTYPE token's force-quirks + flag to on.) */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'unexpected-char-in-doctype' + )); + $state = 'bogusDoctype'; + } + break; + + case 'bogusDoctype': + /* Consume the next input character: */ + $char = $this->stream->char(); + + if ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the DOCTYPE token. Switch to the data state. */ + $this->emitToken($this->token); + $state = 'data'; + + } elseif($char === false) { + /* EOF + Emit the DOCTYPE token. Reconsume the EOF character in + the data state. */ + $this->emitToken($this->token); + $this->stream->unget(); + $state = 'data'; + + } else { + /* Anything else + Stay in the bogus DOCTYPE state. */ + } + break; + + // case 'cdataSection': + + } + } + } + + /** + * Returns a serialized representation of the tree. + */ + public function save() { + return $this->tree->save(); + } + + /** + * Returns the input stream. + */ + public function stream() { + return $this->stream; + } + + private function consumeCharacterReference($allowed = false, $inattr = false) { + // This goes quite far against spec, and is far closer to the Python + // impl., mainly because we don't do the large unconsuming the spec + // requires. + + // All consumed characters. + $chars = $this->stream->char(); + + /* This section defines how to consume a character + reference. This definition is used when parsing character + references in text and in attributes. + + The behavior depends on the identity of the next character + (the one immediately after the U+0026 AMPERSAND character): */ + + if ( + $chars[0] === "\x09" || + $chars[0] === "\x0A" || + $chars[0] === "\x0C" || + $chars[0] === "\x20" || + $chars[0] === '<' || + $chars[0] === '&' || + $chars === false || + $chars[0] === $allowed + ) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000C FORM FEED (FF) + U+0020 SPACE + U+003C LESS-THAN SIGN + U+0026 AMPERSAND + EOF + The additional allowed character, if there is one + Not a character reference. No characters are consumed, + and nothing is returned. (This is not an error, either.) */ + // We already consumed, so unconsume. + $this->stream->unget(); + return '&'; + } elseif ($chars[0] === '#') { + /* Consume the U+0023 NUMBER SIGN. */ + // Um, yeah, we already did that. + /* The behavior further depends on the character after + the U+0023 NUMBER SIGN: */ + $chars .= $this->stream->char(); + if (isset($chars[1]) && ($chars[1] === 'x' || $chars[1] === 'X')) { + /* U+0078 LATIN SMALL LETTER X + U+0058 LATIN CAPITAL LETTER X */ + /* Consume the X. */ + // Um, yeah, we already did that. + /* Follow the steps below, but using the range of + characters U+0030 DIGIT ZERO through to U+0039 DIGIT + NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + A, through to U+0046 LATIN CAPITAL LETTER F (in other + words, 0123456789, ABCDEF, abcdef). */ + $char_class = self::HEX; + /* When it comes to interpreting the + number, interpret it as a hexadecimal number. */ + $hex = true; + } else { + /* Anything else */ + // Unconsume because we shouldn't have consumed this. + $chars = $chars[0]; + $this->stream->unget(); + /* Follow the steps below, but using the range of + characters U+0030 DIGIT ZERO through to U+0039 DIGIT + NINE (i.e. just 0123456789). */ + $char_class = self::DIGIT; + /* When it comes to interpreting the number, + interpret it as a decimal number. */ + $hex = false; + } + + /* Consume as many characters as match the range of characters given above. */ + $consumed = $this->stream->charsWhile($char_class); + if ($consumed === '' || $consumed === false) { + /* If no characters match the range, then don't consume + any characters (and unconsume the U+0023 NUMBER SIGN + character and, if appropriate, the X character). This + is a parse error; nothing is returned. */ + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-numeric-entity' + )); + return '&' . $chars; + } else { + /* Otherwise, if the next character is a U+003B SEMICOLON, + consume that too. If it isn't, there is a parse error. */ + if ($this->stream->char() !== ';') { + $this->stream->unget(); + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'numeric-entity-without-semicolon' + )); + } + + /* If one or more characters match the range, then take + them all and interpret the string of characters as a number + (either hexadecimal or decimal as appropriate). */ + $codepoint = $hex ? hexdec($consumed) : (int) $consumed; + + /* If that number is one of the numbers in the first column + of the following table, then this is a parse error. Find the + row with that number in the first column, and return a + character token for the Unicode character given in the + second column of that row. */ + $new_codepoint = HTML5_Data::getRealCodepoint($codepoint); + if ($new_codepoint) { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'illegal-windows-1252-entity' + )); + $codepoint = $new_codepoint; + } else { + /* Otherwise, if the number is in the range 0x0000 to 0x0008, + U+000B, U+000E to 0x001F, 0x007F to 0x009F, 0xD800 to 0xDFFF , + 0xFDD0 to 0xFDEF, or is one of 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, + 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, + 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, + 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, + 0xCFFFF, 0xDFFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, + 0x10FFFE, or 0x10FFFF, or is higher than 0x10FFFF, then this + is a parse error; return a character token for the U+FFFD + REPLACEMENT CHARACTER character instead. */ + // && has higher precedence than || + if ( + $codepoint >= 0x0000 && $codepoint <= 0x0008 || + $codepoint === 0x000B || + $codepoint >= 0x000E && $codepoint <= 0x001F || + $codepoint >= 0x007F && $codepoint <= 0x009F || + $codepoint >= 0xD800 && $codepoint <= 0xDFFF || + $codepoint >= 0xFDD0 && $codepoint <= 0xFDEF || + ($codepoint & 0xFFFE) === 0xFFFE || + $codepoint > 0x10FFFF + ) { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'illegal-codepoint-for-numeric-entity' + )); + $codepoint = 0xFFFD; + } + } + + /* Otherwise, return a character token for the Unicode + character whose code point is that number. */ + return HTML5_Data::utf8chr($codepoint); + } + + } else { + /* Anything else */ + + /* Consume the maximum number of characters possible, + with the consumed characters matching one of the + identifiers in the first column of the named character + references table (in a case-sensitive manner). */ + + // we will implement this by matching the longest + // alphanumeric + semicolon string, and then working + // our way backwards + $chars .= $this->stream->charsWhile(self::DIGIT . self::ALPHA . ';', HTML5_Data::getNamedCharacterReferenceMaxLength() - 1); + $len = strlen($chars); + + $refs = HTML5_Data::getNamedCharacterReferences(); + $codepoint = false; + for($c = $len; $c > 0; $c--) { + $id = substr($chars, 0, $c); + if(isset($refs[$id])) { + $codepoint = $refs[$id]; + break; + } + } + + /* If no match can be made, then this is a parse error. + No characters are consumed, and nothing is returned. */ + if (!$codepoint) { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'expected-named-entity' + )); + return '&' . $chars; + } + + /* If the last character matched is not a U+003B SEMICOLON + (;), there is a parse error. */ + $semicolon = true; + if (substr($id, -1) !== ';') { + $this->emitToken(array( + 'type' => self::PARSEERROR, + 'data' => 'named-entity-without-semicolon' + )); + $semicolon = false; + } + + + /* If the character reference is being consumed as part of + an attribute, and the last character matched is not a + U+003B SEMICOLON (;), and the next character is in the + range U+0030 DIGIT ZERO to U+0039 DIGIT NINE, U+0041 + LATIN CAPITAL LETTER A to U+005A LATIN CAPITAL LETTER Z, + or U+0061 LATIN SMALL LETTER A to U+007A LATIN SMALL LETTER Z, + then, for historical reasons, all the characters that were + matched after the U+0026 AMPERSAND (&) must be unconsumed, + and nothing is returned. */ + if ( + $inattr && !$semicolon && + strspn(substr($chars, $c, 1), self::ALPHA . self::DIGIT) + ) { + return '&' . $chars; + } + + /* Otherwise, return a character token for the character + corresponding to the character reference name (as given + by the second column of the named character references table). */ + return HTML5_Data::utf8chr($codepoint) . substr($chars, $c); + } + } + + private function characterReferenceInAttributeValue($allowed = false) { + /* Attempt to consume a character reference. */ + $entity = $this->consumeCharacterReference($allowed, true); + + /* If nothing is returned, append a U+0026 AMPERSAND + character to the current attribute's value. + + Otherwise, append the returned character token to the + current attribute's value. */ + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + /* Finally, switch back to the attribute value state that you + were in when were switched into this state. */ + } + + /** + * Emits a token, passing it on to the tree builder. + */ + protected function emitToken($token, $checkStream = true) { + if ($checkStream) { + // Emit errors from input stream. + while ($this->stream->errors) { + $this->emitToken(array_shift($this->stream->errors), false); + } + } + + // the current structure of attributes is not a terribly good one + $this->tree->emitToken($token); + + if(is_int($this->tree->content_model)) { + $this->content_model = $this->tree->content_model; + $this->tree->content_model = null; + + } elseif($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } +} + diff --git a/library/HTML5/TreeBuilder.php b/library/HTML5/TreeBuilder.php new file mode 100644 index 0000000..03e2ee7 --- /dev/null +++ b/library/HTML5/TreeBuilder.php @@ -0,0 +1,3715 @@ + +Copyright 2009 Edward Z. Yang + +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. + +*/ + +// Tags for FIX ME!!!: (in order of priority) +// XXX - should be fixed NAO! +// XERROR - with regards to parse errors +// XSCRIPT - with regards to scripting mode +// XENCODING - with regards to encoding (for reparsing tests) + +class HTML5_TreeBuilder { + public $stack = array(); + public $content_model; + + private $mode; + private $original_mode; + private $secondary_mode; + private $dom; + // Whether or not normal insertion of nodes should actually foster + // parent (used in one case in spec) + private $foster_parent = false; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $flag_frameset_ok = true; + private $flag_force_quirks = false; + private $ignored = false; + private $quirks_mode = null; + // this gets to 2 when we want to ignore the next lf character, and + // is decrement at the beginning of each processed token (this way, + // code can check for (bool)$ignore_lf_token, but it phases out + // appropriately) + private $ignore_lf_token = 0; + private $fragment = false; + private $root; + + private $scoping = array('applet','button','caption','html','marquee','object','table','td','th', 'svg:foreignObject'); + private $formatting = array('a','b','big','code','em','font','i','nobr','s','small','strike','strong','tt','u'); + private $special = array('address','area','article','aside','base','basefont','bgsound', + 'blockquote','body','br','center','col','colgroup','command','dd','details','dialog','dir','div','dl', + 'dt','embed','fieldset','figure','footer','form','frame','frameset','h1','h2','h3','h4','h5', + 'h6','head','header','hgroup','hr','iframe','img','input','isindex','li','link', + 'listing','menu','meta','nav','noembed','noframes','noscript','ol', + 'p','param','plaintext','pre','script','select','spacer','style', + 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); + + // Tree construction modes + const INITIAL = 0; + const BEFORE_HTML = 1; + const BEFORE_HEAD = 2; + const IN_HEAD = 3; + const IN_HEAD_NOSCRIPT = 4; + const AFTER_HEAD = 5; + const IN_BODY = 6; + const IN_CDATA_RCDATA = 7; + const IN_TABLE = 8; + const IN_CAPTION = 9; + const IN_COLUMN_GROUP = 10; + const IN_TABLE_BODY = 11; + const IN_ROW = 12; + const IN_CELL = 13; + const IN_SELECT = 14; + const IN_SELECT_IN_TABLE= 15; + const IN_FOREIGN_CONTENT= 16; + const AFTER_BODY = 17; + const IN_FRAMESET = 18; + const AFTER_FRAMESET = 19; + const AFTER_AFTER_BODY = 20; + const AFTER_AFTER_FRAMESET = 21; + + /** + * Converts a magic number to a readable name. Use for debugging. + */ + private function strConst($number) { + static $lookup; + if (!$lookup) { + $r = new ReflectionClass('HTML5_TreeBuilder'); + $lookup = array_flip($r->getConstants()); + } + return $lookup[$number]; + } + + // The different types of elements. + const SPECIAL = 100; + const SCOPING = 101; + const FORMATTING = 102; + const PHRASING = 103; + + // Quirks modes in $quirks_mode + const NO_QUIRKS = 200; + const QUIRKS_MODE = 201; + const LIMITED_QUIRKS_MODE = 202; + + // Marker to be placed in $a_formatting + const MARKER = 300; + + // Namespaces for foreign content + const NS_HTML = null; // to prevent DOM from requiring NS on everything + const NS_MATHML = 'http://www.w3.org/1998/Math/MathML'; + const NS_SVG = 'http://www.w3.org/2000/svg'; + const NS_XLINK = 'http://www.w3.org/1999/xlink'; + const NS_XML = 'http://www.w3.org/XML/1998/namespace'; + const NS_XMLNS = 'http://www.w3.org/2000/xmlns/'; + + public function __construct() { + $this->mode = self::INITIAL; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token, $mode = null) { + // XXX: ignore parse errors... why are we emitting them, again? + if ($token['type'] === HTML5_Tokenizer::PARSEERROR) return; + if ($mode === null) $mode = $this->mode; + + /* + $backtrace = debug_backtrace(); + if ($backtrace[1]['class'] !== 'HTML5_TreeBuilder') echo "--\n"; + echo $this->strConst($mode); + if ($this->original_mode) echo " (originally ".$this->strConst($this->original_mode).")"; + echo "\n "; + token_dump($token); + $this->printStack(); + $this->printActiveFormattingElements(); + if ($this->foster_parent) echo " -> this is a foster parent mode\n"; + */ + + if ($this->ignore_lf_token) $this->ignore_lf_token--; + $this->ignored = false; + // indenting is a little wonky, this can be changed later on + switch ($mode) { + + case self::INITIAL: + + /* A character token that is one of U+0009 CHARACTER TABULATION, + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020 SPACE */ + if ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) { + /* Ignore the token. */ + $this->ignored = true; + } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) { + if ( + $token['name'] !== 'html' || !empty($token['public']) || + !empty($token['system']) || $token !== 'about:legacy-compat' + ) { + /* If the DOCTYPE token's name is not a case-sensitive match + * for the string "html", or if the token's public identifier + * is not missing, or if the token's system identifier is + * neither missing nor a case-sensitive match for the string + * "about:legacy-compat", then there is a parse error (this + * is the DOCTYPE parse error). */ + // DOCTYPE parse error + } + /* Append a DocumentType node to the Document node, with the name + * attribute set to the name given in the DOCTYPE token, or the + * empty string if the name was missing; the publicId attribute + * set to the public identifier given in the DOCTYPE token, or + * the empty string if the public identifier was missing; the + * systemId attribute set to the system identifier given in the + * DOCTYPE token, or the empty string if the system identifier + * was missing; and the other attributes specific to + * DocumentType objects set to null and empty lists as + * appropriate. Associate the DocumentType node with the + * Document object so that it is returned as the value of the + * doctype attribute of the Document object. */ + if (!isset($token['public'])) $token['public'] = null; + if (!isset($token['system'])) $token['system'] = null; + // Yes this is hacky. I'm kind of annoyed that I can't appendChild + // a doctype to DOMDocument. Maybe I haven't chanted the right + // syllables. + $impl = new DOMImplementation(); + // This call can fail for particularly pathological cases (namely, + // the qualifiedName parameter ($token['name']) could be missing. + if ($token['name']) { + $doctype = $impl->createDocumentType($token['name'], $token['public'], $token['system']); + $this->dom->appendChild($doctype); + } else { + // It looks like libxml's not actually *able* to express this case. + // So... don't. + $this->dom->emptyDoctype = true; + } + $public = is_null($token['public']) ? false : strtolower($token['public']); + $system = is_null($token['system']) ? false : strtolower($token['system']); + $publicStartsWithForQuirks = array( + "+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//", + ); + $publicSetToForQuirks = array( + "-//w3o//dtd w3 html strict 3.0//", + "-/w3c/dtd html 4.0 transitional/en", + "html", + ); + $publicStartsWithAndSystemForQuirks = array( + "-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//", + ); + $publicStartsWithForLimitedQuirks = array( + "-//w3c//dtd xhtml 1.0 frameset//", + "-//w3c//dtd xhtml 1.0 transitional//", + ); + $publicStartsWithAndSystemForLimitedQuirks = array( + "-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//", + ); + // first, do easy checks + if ( + !empty($token['force-quirks']) || + strtolower($token['name']) !== 'html' + ) { + $this->quirks_mode = self::QUIRKS_MODE; + } else { + do { + if ($system) { + foreach ($publicStartsWithAndSystemForQuirks as $x) { + if (strncmp($public, $x, strlen($x)) === 0) { + $this->quirks_mode = self::QUIRKS_MODE; + break; + } + } + if (!is_null($this->quirks_mode)) break; + foreach ($publicStartsWithAndSystemForLimitedQuirks as $x) { + if (strncmp($public, $x, strlen($x)) === 0) { + $this->quirks_mode = self::LIMITED_QUIRKS_MODE; + break; + } + } + if (!is_null($this->quirks_mode)) break; + } + foreach ($publicSetToForQuirks as $x) { + if ($public === $x) { + $this->quirks_mode = self::QUIRKS_MODE; + break; + } + } + if (!is_null($this->quirks_mode)) break; + foreach ($publicStartsWithForLimitedQuirks as $x) { + if (strncmp($public, $x, strlen($x)) === 0) { + $this->quirks_mode = self::LIMITED_QUIRKS_MODE; + } + } + if (!is_null($this->quirks_mode)) break; + if ($system === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") { + $this->quirks_mode = self::QUIRKS_MODE; + break; + } + foreach ($publicStartsWithForQuirks as $x) { + if (strncmp($public, $x, strlen($x)) === 0) { + $this->quirks_mode = self::QUIRKS_MODE; + break; + } + } + if (is_null($this->quirks_mode)) { + $this->quirks_mode = self::NO_QUIRKS; + } + } while (false); + } + $this->mode = self::BEFORE_HTML; + } else { + // parse error + /* Switch the insertion mode to "before html", then reprocess the + * current token. */ + $this->mode = self::BEFORE_HTML; + $this->quirks_mode = self::QUIRKS_MODE; + $this->emitToken($token); + } + break; + + case self::BEFORE_HTML: + + /* A DOCTYPE token */ + if($token['type'] === HTML5_Tokenizer::DOCTYPE) { + // Parse error. Ignore the token. + $this->ignored = true; + + /* A comment token */ + } elseif($token['type'] === HTML5_Tokenizer::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif($token['type'] === HTML5_Tokenizer::SPACECHARACTER) { + /* Ignore the token. */ + $this->ignored = true; + + /* A start tag whose tag name is "html" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] == 'html') { + /* Create an element for the token in the HTML namespace. Append it + * to the Document object. Put this element in the stack of open + * elements. */ + $html = $this->insertElement($token, false); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->mode = self::BEFORE_HEAD; + + } else { + /* Create an html element. Append it to the Document object. Put + * this element in the stack of open elements. */ + $html = $this->dom->createElementNS(self::NS_HTML, 'html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + /* Switch the insertion mode to "before head", then reprocess the + * current token. */ + $this->mode = self::BEFORE_HEAD; + $this->emitToken($token); + } + break; + + case self::BEFORE_HEAD: + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) { + /* Ignore the token. */ + $this->ignored = true; + + /* A comment token */ + } elseif($token['type'] === HTML5_Tokenizer::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A DOCTYPE token */ + } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) { + /* Parse error. Ignore the token */ + $this->ignored = true; + // parse error + + /* A start tag token with the tag name "html" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') { + /* Process the token using the rules for the "in body" + * insertion mode. */ + $this->processWithRulesFor($token, self::IN_BODY); + + /* A start tag token with the tag name "head" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') { + /* Insert an HTML element for the token. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* An end tag whose tag name is one of: "head", "body", "html", "br" */ + } elseif( + $token['type'] === HTML5_Tokenizer::ENDTAG && ( + $token['name'] === 'head' || $token['name'] === 'body' || + $token['name'] === 'html' || $token['name'] === 'br' + )) { + /* Act as if a start tag token with the tag name "head" and no + * attributes had been seen, then reprocess the current token. */ + $this->emitToken(array( + 'name' => 'head', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + $this->emitToken($token); + + /* Any other end tag */ + } elseif($token['type'] === HTML5_Tokenizer::ENDTAG) { + /* Parse error. Ignore the token. */ + $this->ignored = true; + + } else { + /* Act as if a start tag token with the tag name "head" and no + * attributes had been seen, then reprocess the current token. + * Note: This will result in an empty head element being + * generated, with the current token being reprocessed in the + * "after head" insertion mode. */ + $this->emitToken(array( + 'name' => 'head', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + $this->emitToken($token); + } + break; + + case self::IN_HEAD: + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. */ + if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) { + /* Insert the character into the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5_Tokenizer::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A DOCTYPE token */ + } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) { + /* Parse error. Ignore the token. */ + $this->ignored = true; + // parse error + + /* A start tag whose tag name is "html" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && + $token['name'] === 'html') { + $this->processWithRulesFor($token, self::IN_BODY); + + /* A start tag whose tag name is one of: "base", "command", "link" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && + ($token['name'] === 'base' || $token['name'] === 'command' || + $token['name'] === 'link')) { + /* Insert an HTML element for the token. Immediately pop the + * current node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + // YYY: Acknowledge the token's self-closing flag, if it is set. + + /* A start tag whose tag name is "meta" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'meta') { + /* Insert an HTML element for the token. Immediately pop the + * current node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + // XERROR: Acknowledge the token's self-closing flag, if it is set. + + // XENCODING: If the element has a charset attribute, and its value is a + // supported encoding, and the confidence is currently tentative, + // then change the encoding to the encoding given by the value of + // the charset attribute. + // + // Otherwise, if the element has a content attribute, and applying + // the algorithm for extracting an encoding from a Content-Type to + // its value returns a supported encoding encoding, and the + // confidence is currently tentative, then change the encoding to + // the encoding encoding. + + /* A start tag with the tag name "title" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'title') { + $this->insertRCDATAElement($token); + + /* A start tag whose tag name is "noscript", if the scripting flag is enabled, or + * A start tag whose tag name is one of: "noframes", "style" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && + ($token['name'] === 'noscript' || $token['name'] === 'noframes' || $token['name'] === 'style')) { + // XSCRIPT: Scripting flag not respected + $this->insertCDATAElement($token); + + // XSCRIPT: Scripting flag disable not implemented + + /* A start tag with the tag name "script" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') { + /* 1. Create an element for the token in the HTML namespace. */ + $node = $this->insertElement($token, false); + + /* 2. Mark the element as being "parser-inserted" */ + // Uhhh... XSCRIPT + + /* 3. If the parser was originally created for the HTML + * fragment parsing algorithm, then mark the script element as + * "already executed". (fragment case) */ + // ditto... XSCRIPT + + /* 4. Append the new element to the current node and push it onto + * the stack of open elements. */ + end($this->stack)->appendChild($node); + $this->stack[] = $node; + // I guess we could squash these together + + /* 6. Let the original insertion mode be the current insertion mode. */ + $this->original_mode = $this->mode; + /* 7. Switch the insertion mode to "in CDATA/RCDATA" */ + $this->mode = self::IN_CDATA_RCDATA; + /* 5. Switch the tokeniser's content model flag to the CDATA state. */ + $this->content_model = HTML5_Tokenizer::CDATA; + + /* An end tag with the tag name "head" */ + } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'head') { + /* Pop the current node (which will be the head element) off the stack of open elements. */ + array_pop($this->stack); + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + // Slight logic inversion here to minimize duplication + /* A start tag with the tag name "head". */ + /* An end tag whose tag name is not one of: "body", "html", "br" */ + } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] !== 'html' && + $token['name'] !== 'body' && $token['name'] !== 'br')) { + // Parse error. Ignore the token. + $this->ignored = true; + + /* Anything else */ + } else { + /* Act as if an end tag token with the tag name "head" had been + * seen, and reprocess the current token. */ + $this->emitToken(array( + 'name' => 'head', + 'type' => HTML5_Tokenizer::ENDTAG + )); + + /* Then, reprocess the current token. */ + $this->emitToken($token); + } + break; + + case self::IN_HEAD_NOSCRIPT: + if ($token['type'] === HTML5_Tokenizer::DOCTYPE) { + // parse error + } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') { + $this->processWithRulesFor($token, self::IN_BODY); + } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'noscript') { + /* Pop the current node (which will be a noscript element) from the + * stack of open elements; the new current node will be a head + * element. */ + array_pop($this->stack); + $this->mode = self::IN_HEAD; + } elseif ( + ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) || + ($token['type'] === HTML5_Tokenizer::COMMENT) || + ($token['type'] === HTML5_Tokenizer::STARTTAG && ( + $token['name'] === 'link' || $token['name'] === 'meta' || + $token['name'] === 'noframes' || $token['name'] === 'style'))) { + $this->processWithRulesFor($token, self::IN_HEAD); + // inverted logic + } elseif ( + ($token['type'] === HTML5_Tokenizer::STARTTAG && ( + $token['name'] === 'head' || $token['name'] === 'noscript')) || + ($token['type'] === HTML5_Tokenizer::ENDTAG && + $token['name'] !== 'br')) { + // parse error + } else { + // parse error + $this->emitToken(array( + 'type' => HTML5_Tokenizer::ENDTAG, + 'name' => 'noscript', + )); + $this->emitToken($token); + } + break; + + case self::AFTER_HEAD: + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5_Tokenizer::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) { + // parse error + + } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') { + $this->processWithRulesFor($token, self::IN_BODY); + + /* A start tag token with the tag name "body" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'body') { + $this->insertElement($token); + + /* Set the frameset-ok flag to "not ok". */ + $this->flag_frameset_ok = false; + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAMESET; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'], + array('base', 'link', 'meta', 'noframes', 'script', 'style', 'title'))) { + // parse error + /* Push the node pointed to by the head element pointer onto the + * stack of open elements. */ + $this->stack[] = $this->head_pointer; + $this->processWithRulesFor($token, self::IN_HEAD); + array_splice($this->stack, array_search($this->head_pointer, $this->stack, true), 1); + + // inversion of specification + } elseif( + ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5_Tokenizer::ENDTAG && + $token['name'] !== 'body' && $token['name'] !== 'html' && + $token['name'] !== 'br')) { + // parse error + + /* Anything else */ + } else { + $this->emitToken(array( + 'name' => 'body', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + $this->flag_frameset_ok = true; + $this->emitToken($token); + } + break; + + case self::IN_BODY: + /* Handle the token as follows: */ + + switch($token['type']) { + /* A character token */ + case HTML5_Tokenizer::CHARACTER: + case HTML5_Tokenizer::SPACECHARACTER: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* If the token is not one of U+0009 CHARACTER TABULATION, + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020 + * SPACE, then set the frameset-ok flag to "not ok". */ + // i.e., if any of the characters is not whitespace + if (strlen($token['data']) !== strspn($token['data'], HTML5_Tokenizer::WHITESPACE)) { + $this->flag_frameset_ok = false; + } + break; + + /* A comment token */ + case HTML5_Tokenizer::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5_Tokenizer::DOCTYPE: + // parse error + break; + + case HTML5_Tokenizer::STARTTAG: + switch($token['name']) { + case 'html': + // parse error + /* For each attribute on the token, check to see if the + * attribute is already present on the top element of the + * stack of open elements. If it is not, add the attribute + * and its corresponding value to that element. */ + foreach($token['attr'] as $attr) { + if(!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + break; + + case 'base': case 'command': case 'link': case 'meta': case 'noframes': + case 'script': case 'style': case 'title': + /* Process the token as if the insertion mode had been "in + head". */ + $this->processWithRulesFor($token, self::IN_HEAD); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (fragment case) */ + if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') { + $this->ignored = true; + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach($token['attr'] as $attr) { + if(!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + case 'frameset': + // parse error + /* If the second element on the stack of open elements is + * not a body element, or, if the stack of open elements + * has only one node on it, then ignore the token. + * (fragment case) */ + if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') { + $this->ignored = true; + // Ignore + } elseif (!$this->flag_frameset_ok) { + $this->ignored = true; + // Ignore + } else { + /* 1. Remove the second element on the stack of open + * elements from its parent node, if it has one. */ + if($this->stack[1]->parentNode) { + $this->stack[1]->parentNode->removeChild($this->stack[1]); + } + + /* 2. Pop all the nodes from the bottom of the stack of + * open elements, from the current node up to the root + * html element. */ + array_splice($this->stack, 1); + + $this->insertElement($token); + $this->mode = self::IN_FRAMESET; + } + break; + + // in spec, there is a diversion here + + case 'address': case 'article': case 'aside': case 'blockquote': + case 'center': case 'datagrid': case 'details': case 'dialog': case 'dir': + case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer': + case 'header': case 'hgroup': case 'menu': case 'nav': + case 'ol': case 'p': case 'section': case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* If the current node is an element whose tag name is one + * of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a + * parse error; pop the current node off the stack of open + * elements. */ + $peek = array_pop($this->stack); + if (in_array($peek->tagName, array("h1", "h2", "h3", "h4", "h5", "h6"))) { + // parse error + } else { + $this->stack[] = $peek; + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + case 'pre': case 'listing': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + $this->insertElement($token); + /* If the next token is a U+000A LINE FEED (LF) character + * token, then ignore that token and move on to the next + * one. (Newlines at the start of pre blocks are ignored as + * an authoring convenience.) */ + $this->ignore_lf_token = 2; + $this->flag_frameset_ok = false; + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if($this->form_pointer !== null) { + $this->ignored = true; + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + // condensed specification + case 'li': case 'dd': case 'dt': + /* 1. Set the frameset-ok flag to "not ok". */ + $this->flag_frameset_ok = false; + + $stack_length = count($this->stack) - 1; + for($n = $stack_length; 0 <= $n; $n--) { + /* 2. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node); + + // for case 'li': + /* 3. If node is an li element, then act as if an end + * tag with the tag name "li" had been seen, then jump + * to the last step. */ + // for case 'dd': case 'dt': + /* If node is a dd or dt element, then act as if an end + * tag with the same tag name as node had been seen, then + * jump to the last step. */ + if(($token['name'] === 'li' && $node->tagName === 'li') || + ($token['name'] !== 'li' && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { // limited conditional + $this->emitToken(array( + 'type' => HTML5_Tokenizer::ENDTAG, + 'name' => $node->tagName, + )); + break; + } + + /* 4. If node is not in the formatting category, and is + not in the phrasing category, and is not an address, + div or p element, then stop this algorithm. */ + if($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div' && + $node->tagName !== 'p') { + break; + } + + /* 5. Otherwise, set node to the previous entry in the + * stack of open elements and return to step 2. */ + } + + /* 6. This is the last step. */ + + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + $this->content_model = HTML5_Tokenizer::PLAINTEXT; + break; + + // more diversions + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for($n = $leng - 1; $n >= 0; $n--) { + if($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif($this->a_formatting[$n]->tagName === 'a') { + $a = $this->a_formatting[$n]; + $this->emitToken(array( + 'name' => 'a', + 'type' => HTML5_Tokenizer::ENDTAG + )); + if (in_array($a, $this->a_formatting)) { + $a_i = array_search($a, $this->a_formatting, true); + if($a_i !== false) array_splice($this->a_formatting, $a_i, 1); + } + if (in_array($a, $this->stack)) { + $a_i = array_search($a, $this->stack, true); + if ($a_i !== false) array_splice($this->stack, $a_i, 1); + } + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + case 'b': case 'big': case 'code': case 'em': case 'font': case 'i': + case 's': case 'small': case 'strike': + case 'strong': case 'tt': case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + case 'nobr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* If the stack of open elements has a nobr element in + * scope, then this is a parse error; act as if an end tag + * with the tag name "nobr" had been seen, then once again + * reconstruct the active formatting elements, if any. */ + if ($this->elementInScope('nobr')) { + $this->emitToken(array( + 'name' => 'nobr', + 'type' => HTML5_Tokenizer::ENDTAG, + )); + $this->reconstructActiveFormattingElements(); + } + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + // another diversion + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) (I hope you're right! -- ezyang) */ + if($this->elementInScope('button')) { + $this->emitToken(array( + 'name' => 'button', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + $this->flag_frameset_ok = false; + break; + + case 'applet': case 'marquee': case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + $this->flag_frameset_ok = false; + break; + + // spec diversion + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->quirks_mode !== self::QUIRKS_MODE && + $this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + $this->flag_frameset_ok = false; + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': case 'basefont': case 'bgsound': case 'br': + case 'embed': case 'img': case 'input': case 'keygen': case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + // YYY: Acknowledge the token's self-closing flag, if it is set. + + $this->flag_frameset_ok = false; + break; + + case 'param': case 'source': + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + // YYY: Acknowledge the token's self-closing flag, if it is set. + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + // YYY: Acknowledge the token's self-closing flag, if it is set. + + $this->flag_frameset_ok = false; + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + $this->emitToken($token); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + + /* If the form element pointer is not null, + then ignore the token. */ + if($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + /* If the token has an attribute called "action", set + * the action attribute on the resulting form + * element to the value of the "action" attribute of + * the token. */ + $attr = array(); + $action = $this->getAttr($token, 'action'); + if ($action !== false) { + $attr[] = array('name' => 'action', 'value' => $action); + } + $this->emitToken(array( + 'name' => 'form', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => $attr + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->emitToken(array( + 'name' => 'hr', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->emitToken(array( + 'name' => 'label', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => array() + )); + + /* Act as if a stream of character tokens had been seen. */ + $prompt = $this->getAttr($token, 'prompt'); + if ($prompt === false) { + $prompt = 'This is a searchable index. '. + 'Insert your search keywords here: '; + } + $this->emitToken(array( + 'data' => $prompt, + 'type' => HTML5_Tokenizer::CHARACTER, + )); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = array(); + foreach ($token['attr'] as $keypair) { + if ($keypair['name'] === 'name' || $keypair['name'] === 'action' || + $keypair['name'] === 'prompt') continue; + $attr[] = $keypair; + } + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->emitToken(array( + 'name' => 'input', + 'type' => HTML5_Tokenizer::STARTTAG, + 'attr' => $attr + )); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->emitToken(array( + 'name' => 'label', + 'type' => HTML5_Tokenizer::ENDTAG + )); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5_Tokenizer::ENDTAG + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->emitToken(array( + 'name' => 'hr', + 'type' => HTML5_Tokenizer::STARTTAG + )); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->emitToken(array( + 'name' => 'form', + 'type' => HTML5_Tokenizer::ENDTAG + )); + } else { + $this->ignored = true; + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* If the next token is a U+000A LINE FEED (LF) + * character token, then ignore that token and move on to + * the next one. (Newlines at the start of textarea + * elements are ignored as an authoring convenience.) + * need flag, see also
     */
    +                    $this->ignore_lf_token = 2;
    +
    +                    $this->original_mode = $this->mode;
    +                    $this->flag_frameset_ok = false;
    +                    $this->mode = self::IN_CDATA_RCDATA;
    +
    +                    /* Switch the tokeniser's content model flag to the
    +                    RCDATA state. */
    +                    $this->content_model = HTML5_Tokenizer::RCDATA;
    +                break;
    +
    +                /* A start tag token whose tag name is "xmp" */
    +                case 'xmp':
    +                    /* Reconstruct the active formatting elements, if any. */
    +                    $this->reconstructActiveFormattingElements();
    +
    +                    $this->flag_frameset_ok = false;
    +
    +                    $this->insertCDATAElement($token);
    +                break;
    +
    +                case 'iframe':
    +                    $this->flag_frameset_ok = false;
    +                    $this->insertCDATAElement($token);
    +                break;
    +
    +                case 'noembed': case 'noscript':
    +                    // XSCRIPT: should check scripting flag
    +                    $this->insertCDATAElement($token);
    +                break;
    +
    +                /* A start tag whose tag name is "select" */
    +                case 'select':
    +                    /* Reconstruct the active formatting elements, if any. */
    +                    $this->reconstructActiveFormattingElements();
    +
    +                    /* Insert an HTML element for the token. */
    +                    $this->insertElement($token);
    +
    +                    $this->flag_frameset_ok = false;
    +
    +                    /* If the insertion mode is one of in table", "in caption",
    +                     * "in column group", "in table body", "in row", or "in
    +                     * cell", then switch the insertion mode to "in select in
    +                     * table". Otherwise, switch the insertion mode  to "in
    +                     * select". */
    +                    if (
    +                        $this->mode === self::IN_TABLE || $this->mode === self::IN_CAPTION ||
    +                        $this->mode === self::IN_COLUMN_GROUP || $this->mode ==+self::IN_TABLE_BODY ||
    +                        $this->mode === self::IN_ROW || $this->mode === self::IN_CELL
    +                    ) {
    +                        $this->mode = self::IN_SELECT_IN_TABLE;
    +                    } else {
    +                        $this->mode = self::IN_SELECT;
    +                    }
    +                break;
    +
    +                case 'option': case 'optgroup':
    +                    if ($this->elementInScope('option')) {
    +                        $this->emitToken(array(
    +                            'name' => 'option',
    +                            'type' => HTML5_Tokenizer::ENDTAG,
    +                        ));
    +                    }
    +                    $this->reconstructActiveFormattingElements();
    +                    $this->insertElement($token);
    +                break;
    +
    +                case 'rp': case 'rt':
    +                    /* If the stack of open elements has a ruby element in scope, then generate
    +                     * implied end tags. If the current node is not then a ruby element, this is
    +                     * a parse error; pop all the nodes from the current node up to the node
    +                     * immediately before the bottommost ruby element on the stack of open elements.
    +                     */
    +                    if ($this->elementInScope('ruby')) {
    +                        $this->generateImpliedEndTags();
    +                    }
    +                    $peek = false;
    +                    do {
    +                        if ($peek) {
    +                            // parse error
    +                        }
    +                        $peek = array_pop($this->stack);
    +                    } while ($peek->tagName !== 'ruby');
    +                    $this->stack[] = $peek; // we popped one too many
    +                    $this->insertElement($token);
    +                break;
    +
    +                // spec diversion
    +
    +                case 'math':
    +                    $this->reconstructActiveFormattingElements();
    +                    $token = $this->adjustMathMLAttributes($token);
    +                    $token = $this->adjustForeignAttributes($token);
    +                    $this->insertForeignElement($token, self::NS_MATHML);
    +                    if (isset($token['self-closing'])) {
    +                        // XERROR: acknowledge the token's self-closing flag
    +                        array_pop($this->stack);
    +                    }
    +                    if ($this->mode !== self::IN_FOREIGN_CONTENT) {
    +                        $this->secondary_mode = $this->mode;
    +                        $this->mode = self::IN_FOREIGN_CONTENT;
    +                    }
    +                break;
    +
    +                case 'svg':
    +                    $this->reconstructActiveFormattingElements();
    +                    $token = $this->adjustSVGAttributes($token);
    +                    $token = $this->adjustForeignAttributes($token);
    +                    $this->insertForeignElement($token, self::NS_SVG);
    +                    if (isset($token['self-closing'])) {
    +                        // XERROR: acknowledge the token's self-closing flag
    +                        array_pop($this->stack);
    +                    }
    +                    if ($this->mode !== self::IN_FOREIGN_CONTENT) {
    +                        $this->secondary_mode = $this->mode;
    +                        $this->mode = self::IN_FOREIGN_CONTENT;
    +                    }
    +                break;
    +
    +                case 'caption': case 'col': case 'colgroup': case 'frame': case 'head':
    +                case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr':
    +                    // parse error
    +                break;
    +
    +                /* A start tag token not covered by the previous entries */
    +                default:
    +                    /* Reconstruct the active formatting elements, if any. */
    +                    $this->reconstructActiveFormattingElements();
    +
    +                    $this->insertElement($token);
    +                    /* This element will be a phrasing  element. */
    +                break;
    +            }
    +            break;
    +
    +            case HTML5_Tokenizer::ENDTAG:
    +            switch($token['name']) {
    +                /* An end tag with the tag name "body" */
    +                case 'body':
    +                    /* If the second element in the stack of open elements is
    +                    not a body element, this is a parse error. Ignore the token.
    +                    (innerHTML case) */
    +                    if(count($this->stack) < 2 || $this->stack[1]->tagName !== 'body') {
    +                        $this->ignored = true;
    +
    +                    /* Otherwise, if there is a node in the stack of open
    +                     * elements that is not either a dd element, a dt
    +                     * element, an li element, an optgroup element, an
    +                     * option element, a p element, an rp element, an rt
    +                     * element, a tbody element, a td element, a tfoot
    +                     * element, a th element, a thead element, a tr element,
    +                     * the body element, or the html element, then this is a
    +                     * parse error. */
    +                    } else {
    +                        // XERROR: implement this check for parse error
    +                    }
    +
    +                    /* Change the insertion mode to "after body". */
    +                    $this->mode = self::AFTER_BODY;
    +                break;
    +
    +                /* An end tag with the tag name "html" */
    +                case 'html':
    +                    /* Act as if an end tag with tag name "body" had been seen,
    +                    then, if that token wasn't ignored, reprocess the current
    +                    token. */
    +                    $this->emitToken(array(
    +                        'name' => 'body',
    +                        'type' => HTML5_Tokenizer::ENDTAG
    +                    ));
    +
    +                    if (!$this->ignored) $this->emitToken($token);
    +                break;
    +
    +                case 'address': case 'article': case 'aside': case 'blockquote':
    +                case 'center': case 'datagrid': case 'details': case 'dir':
    +                case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer':
    +                case 'header': case 'hgroup': case 'listing': case 'menu':
    +                case 'nav': case 'ol': case 'pre': case 'section': case 'ul':
    +                    /* If the stack of open elements has an element in scope
    +                    with the same tag name as that of the token, then generate
    +                    implied end tags. */
    +                    if($this->elementInScope($token['name'])) {
    +                        $this->generateImpliedEndTags();
    +
    +                        /* Now, if the current node is not an element with
    +                        the same tag name as that of the token, then this
    +                        is a parse error. */
    +                        // XERROR: implement parse error logic
    +
    +                        /* If the stack of open elements has an element in
    +                        scope with the same tag name as that of the token,
    +                        then pop elements from this stack until an element
    +                        with that tag name has been popped from the stack. */
    +                        do {
    +                            $node = array_pop($this->stack);
    +                        } while ($node->tagName !== $token['name']);
    +                    } else {
    +                        // parse error
    +                    }
    +                break;
    +
    +                /* An end tag whose tag name is "form" */
    +                case 'form':
    +                    /* Let node be the element that the form element pointer is set to. */
    +                    $node = $this->form_pointer;
    +                    /* Set the form element pointer  to null. */
    +                    $this->form_pointer = null;
    +                    /* If node is null or the stack of open elements does not 
    +                        * have node in scope, then this is a parse error; ignore the token. */
    +                    if ($node === null || !in_array($node, $this->stack)) {
    +                        // parse error
    +                        $this->ignored = true;
    +                    } else {
    +                        /* 1. Generate implied end tags. */
    +                        $this->generateImpliedEndTags();
    +                        /* 2. If the current node is not node, then this is a parse error.  */
    +                        if (end($this->stack) !== $node) {
    +                            // parse error
    +                        }
    +                        /* 3. Remove node from the stack of open elements. */
    +                        array_splice($this->stack, array_search($node, $this->stack, true), 1);
    +                    }
    +
    +                break;
    +
    +                /* An end tag whose tag name is "p" */
    +                case 'p':
    +                    /* If the stack of open elements has a p element in scope,
    +                    then generate implied end tags, except for p elements. */
    +                    if($this->elementInScope('p')) {
    +                        /* Generate implied end tags, except for elements with
    +                         * the same tag name as the token. */
    +                        $this->generateImpliedEndTags(array('p'));
    +
    +                        /* If the current node is not a p element, then this is
    +                        a parse error. */
    +                        // XERROR: implement
    +
    +                        /* Pop elements from the stack of open elements  until
    +                         * an element with the same tag name as the token has
    +                         * been popped from the stack. */
    +                        do {
    +                            $node = array_pop($this->stack);
    +                        } while ($node->tagName !== 'p');
    +
    +                    } else {
    +                        // parse error
    +                        $this->emitToken(array(
    +                            'name' => 'p',
    +                            'type' => HTML5_Tokenizer::STARTTAG,
    +                        ));
    +                        $this->emitToken($token);
    +                    }
    +                break;
    +
    +                /* An end tag whose tag name is "dd", "dt", or "li" */
    +                case 'dd': case 'dt': case 'li':
    +                    if($this->elementInScope($token['name'])) {
    +                        $this->generateImpliedEndTags(array($token['name']));
    +
    +                        /* If the current node is not an element with the same
    +                        tag name as the token, then this is a parse error. */
    +                        // XERROR: implement parse error
    +
    +                        /* Pop elements from the stack of open elements  until
    +                         * an element with the same tag name as the token has
    +                         * been popped from the stack. */
    +                        do {
    +                            $node = array_pop($this->stack);
    +                        } while ($node->tagName !== $token['name']);
    +
    +                    } else {
    +                        // parse error
    +                    }
    +                break;
    +
    +                /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
    +                "h5", "h6" */
    +                case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
    +                    $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
    +
    +                    /* If the stack of open elements has in scope an element whose
    +                    tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
    +                    generate implied end tags. */
    +                    if($this->elementInScope($elements)) {
    +                        $this->generateImpliedEndTags();
    +
    +                        /* Now, if the current node is not an element with the same
    +                        tag name as that of the token, then this is a parse error. */
    +                        // XERROR: implement parse error
    +
    +                        /* If the stack of open elements has in scope an element
    +                        whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
    +                        "h6", then pop elements from the stack until an element
    +                        with one of those tag names has been popped from the stack. */
    +                        do {
    +                            $node = array_pop($this->stack);
    +                        } while (!in_array($node->tagName, $elements));
    +                    } else {
    +                        // parse error
    +                    }
    +                break;
    +
    +                /* An end tag whose tag name is one of: "a", "b", "big", "em",
    +                "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
    +                case 'a': case 'b': case 'big': case 'code': case 'em': case 'font':
    +                case 'i': case 'nobr': case 's': case 'small': case 'strike':
    +                case 'strong': case 'tt': case 'u':
    +                    // XERROR: generally speaking this needs parse error logic
    +                    /* 1. Let the formatting element be the last element in
    +                    the list of active formatting elements that:
    +                        * is between the end of the list and the last scope
    +                        marker in the list, if any, or the start of the list
    +                        otherwise, and
    +                        * has the same tag name as the token.
    +                    */
    +                    while(true) {
    +                        for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
    +                            if($this->a_formatting[$a] === self::MARKER) {
    +                                break;
    +
    +                            } elseif($this->a_formatting[$a]->tagName === $token['name']) {
    +                                $formatting_element = $this->a_formatting[$a];
    +                                $in_stack = in_array($formatting_element, $this->stack, true);
    +                                $fe_af_pos = $a;
    +                                break;
    +                            }
    +                        }
    +
    +                        /* If there is no such node, or, if that node is
    +                        also in the stack of open elements but the element
    +                        is not in scope, then this is a parse error. Abort
    +                        these steps. The token is ignored. */
    +                        if(!isset($formatting_element) || ($in_stack &&
    +                        !$this->elementInScope($token['name']))) {
    +                            $this->ignored = true;
    +                            break;
    +
    +                        /* Otherwise, if there is such a node, but that node
    +                        is not in the stack of open elements, then this is a
    +                        parse error; remove the element from the list, and
    +                        abort these steps. */
    +                        } elseif(isset($formatting_element) && !$in_stack) {
    +                            unset($this->a_formatting[$fe_af_pos]);
    +                            $this->a_formatting = array_merge($this->a_formatting);
    +                            break;
    +                        }
    +
    +                        /* Otherwise, there is a formatting element and that
    +                         * element is in the stack and is in scope. If the
    +                         * element is not the current node, this is a parse
    +                         * error. In any case, proceed with the algorithm as
    +                         * written in the following steps. */
    +                        // XERROR: implement me
    +
    +                        /* 2. Let the furthest block be the topmost node in the
    +                        stack of open elements that is lower in the stack
    +                        than the formatting element, and is not an element in
    +                        the phrasing or formatting categories. There might
    +                        not be one. */
    +                        $fe_s_pos = array_search($formatting_element, $this->stack, true);
    +                        $length = count($this->stack);
    +
    +                        for($s = $fe_s_pos + 1; $s < $length; $s++) {
    +                            $category = $this->getElementCategory($this->stack[$s]);
    +
    +                            if($category !== self::PHRASING && $category !== self::FORMATTING) {
    +                                $furthest_block = $this->stack[$s];
    +                                break;
    +                            }
    +                        }
    +
    +                        /* 3. If there is no furthest block, then the UA must
    +                        skip the subsequent steps and instead just pop all
    +                        the nodes from the bottom of the stack of open
    +                        elements, from the current node up to the formatting
    +                        element, and remove the formatting element from the
    +                        list of active formatting elements. */
    +                        if(!isset($furthest_block)) {
    +                            for($n = $length - 1; $n >= $fe_s_pos; $n--) {
    +                                array_pop($this->stack);
    +                            }
    +
    +                            unset($this->a_formatting[$fe_af_pos]);
    +                            $this->a_formatting = array_merge($this->a_formatting);
    +                            break;
    +                        }
    +
    +                        /* 4. Let the common ancestor be the element
    +                        immediately above the formatting element in the stack
    +                        of open elements. */
    +                        $common_ancestor = $this->stack[$fe_s_pos - 1];
    +
    +                        /* 5. Let a bookmark note the position of the
    +                        formatting element in the list of active formatting
    +                        elements relative to the elements on either side
    +                        of it in the list. */
    +                        $bookmark = $fe_af_pos;
    +
    +                        /* 6. Let node and last node  be the furthest block.
    +                        Follow these steps: */
    +                        $node = $furthest_block;
    +                        $last_node = $furthest_block;
    +
    +                        while(true) {
    +                            for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
    +                                /* 6.1 Let node be the element immediately
    +                                prior to node in the stack of open elements. */
    +                                $node = $this->stack[$n];
    +
    +                                /* 6.2 If node is not in the list of active
    +                                formatting elements, then remove node from
    +                                the stack of open elements and then go back
    +                                to step 1. */
    +                                if(!in_array($node, $this->a_formatting, true)) {
    +                                    array_splice($this->stack, $n, 1);
    +
    +                                } else {
    +                                    break;
    +                                }
    +                            }
    +
    +                            /* 6.3 Otherwise, if node is the formatting
    +                            element, then go to the next step in the overall
    +                            algorithm. */
    +                            if($node === $formatting_element) {
    +                                break;
    +
    +                            /* 6.4 Otherwise, if last node is the furthest
    +                            block, then move the aforementioned bookmark to
    +                            be immediately after the node in the list of
    +                            active formatting elements. */
    +                            } elseif($last_node === $furthest_block) {
    +                                $bookmark = array_search($node, $this->a_formatting, true) + 1;
    +                            }
    +
    +                            /* 6.5 Create an element for the token for which
    +                             * the element node was created, replace the entry
    +                             * for node in the list of active formatting
    +                             * elements with an entry for the new element,
    +                             * replace the entry for node in the stack of open
    +                             * elements with an entry for the new element, and
    +                             * let node be the new element. */
    +                            // we don't know what the token is anymore
    +                            $clone = $node->cloneNode();
    +                            $a_pos = array_search($node, $this->a_formatting, true);
    +                            $s_pos = array_search($node, $this->stack, true);
    +                            $this->a_formatting[$a_pos] = $clone;
    +                            $this->stack[$s_pos] = $clone;
    +                            $node = $clone;
    +
    +                            /* 6.6 Insert last node into node, first removing
    +                            it from its previous parent node if any. */
    +                            if($last_node->parentNode !== null) {
    +                                $last_node->parentNode->removeChild($last_node);
    +                            }
    +
    +                            $node->appendChild($last_node);
    +
    +                            /* 6.7 Let last node be node. */
    +                            $last_node = $node;
    +
    +                            /* 6.8 Return to step 1 of this inner set of steps. */
    +                        }
    +
    +                        /* 7. If the common ancestor node is a table, tbody,
    +                         * tfoot, thead, or tr element, then, foster parent
    +                         * whatever last node ended up being in the previous
    +                         * step, first removing it from its previous parent
    +                         * node if any. */
    +                        if ($last_node->parentNode) { // common step
    +                            $last_node->parentNode->removeChild($last_node);
    +                        }
    +                        if (in_array($common_ancestor->tagName, array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
    +                            $this->fosterParent($last_node);
    +                        /* Otherwise, append whatever last node  ended up being
    +                         * in the previous step to the common ancestor node,
    +                         * first removing it from its previous parent node if
    +                         * any. */
    +                        } else {
    +                            $common_ancestor->appendChild($last_node);
    +                        }
    +
    +                        /* 8. Create an element for the token for which the
    +                         * formatting element was created. */
    +                        $clone = $formatting_element->cloneNode();
    +
    +                        /* 9. Take all of the child nodes of the furthest
    +                        block and append them to the element created in the
    +                        last step. */
    +                        while($furthest_block->hasChildNodes()) {
    +                            $child = $furthest_block->firstChild;
    +                            $furthest_block->removeChild($child);
    +                            $clone->appendChild($child);
    +                        }
    +
    +                        /* 10. Append that clone to the furthest block. */
    +                        $furthest_block->appendChild($clone);
    +
    +                        /* 11. Remove the formatting element from the list
    +                        of active formatting elements, and insert the new element
    +                        into the list of active formatting elements at the
    +                        position of the aforementioned bookmark. */
    +                        $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
    +                        array_splice($this->a_formatting, $fe_af_pos, 1);
    +
    +                        $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
    +                        $af_part2 = array_slice($this->a_formatting, $bookmark);
    +                        $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
    +
    +                        /* 12. Remove the formatting element from the stack
    +                        of open elements, and insert the new element into the stack
    +                        of open elements immediately below the position of the
    +                        furthest block in that stack. */
    +                        $fe_s_pos = array_search($formatting_element, $this->stack, true);
    +                        array_splice($this->stack, $fe_s_pos, 1);
    +
    +                        $fb_s_pos = array_search($furthest_block, $this->stack, true);
    +                        $s_part1 = array_slice($this->stack, 0, $fb_s_pos + 1);
    +                        $s_part2 = array_slice($this->stack, $fb_s_pos + 1);
    +                        $this->stack = array_merge($s_part1, array($clone), $s_part2);
    +
    +                        /* 13. Jump back to step 1 in this series of steps. */
    +                        unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
    +                    }
    +                break;
    +
    +                case 'applet': case 'button': case 'marquee': case 'object':
    +                    /* If the stack of open elements has an element in scope whose
    +                    tag name matches the tag name of the token, then generate implied
    +                    tags. */
    +                    if($this->elementInScope($token['name'])) {
    +                        $this->generateImpliedEndTags();
    +
    +                        /* Now, if the current node is not an element with the same
    +                        tag name as the token, then this is a parse error. */
    +                        // XERROR: implement logic
    +
    +                        /* Pop elements from the stack of open elements  until
    +                         * an element with the same tag name as the token has
    +                         * been popped from the stack. */
    +                        do {
    +                            $node = array_pop($this->stack);
    +                        } while ($node->tagName !== $token['name']);
    +
    +                        /* Clear the list of active formatting elements up to the
    +                         * last marker. */
    +                        $keys = array_keys($this->a_formatting, self::MARKER, true);
    +                        $marker = end($keys);
    +
    +                        for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
    +                            array_pop($this->a_formatting);
    +                        }
    +                    } else {
    +                        // parse error
    +                    }
    +                break;
    +
    +                case 'br':
    +                    // Parse error
    +                    $this->emitToken(array(
    +                        'name' => 'br',
    +                        'type' => HTML5_Tokenizer::STARTTAG,
    +                    ));
    +                break;
    +
    +                /* An end tag token not covered by the previous entries */
    +                default:
    +                    for($n = count($this->stack) - 1; $n >= 0; $n--) {
    +                        /* Initialise node to be the current node (the bottommost
    +                        node of the stack). */
    +                        $node = $this->stack[$n];
    +
    +                        /* If node has the same tag name as the end tag token,
    +                        then: */
    +                        if($token['name'] === $node->tagName) {
    +                            /* Generate implied end tags. */
    +                            $this->generateImpliedEndTags();
    +
    +                            /* If the tag name of the end tag token does not
    +                            match the tag name of the current node, this is a
    +                            parse error. */
    +                            // XERROR: implement this
    +
    +                            /* Pop all the nodes from the current node up to
    +                            node, including node, then stop these steps. */
    +                            // XSKETCHY
    +                            do {
    +                                $pop = array_pop($this->stack);
    +                            } while ($pop !== $node);
    +                            break;
    +
    +                        } else {
    +                            $category = $this->getElementCategory($node);
    +
    +                            if($category !== self::FORMATTING && $category !== self::PHRASING) {
    +                                /* Otherwise, if node is in neither the formatting
    +                                category nor the phrasing category, then this is a
    +                                parse error. Stop this algorithm. The end tag token
    +                                is ignored. */
    +                                $this->ignored = true;
    +                                break;
    +                                // parse error
    +                            }
    +                        }
    +                        /* Set node to the previous entry in the stack of open elements. Loop. */
    +                    }
    +                break;
    +            }
    +            break;
    +        }
    +        break;
    +
    +    case self::IN_CDATA_RCDATA:
    +        if (
    +            $token['type'] === HTML5_Tokenizer::CHARACTER ||
    +            $token['type'] === HTML5_Tokenizer::SPACECHARACTER
    +        ) {
    +            $this->insertText($token['data']);
    +        } elseif ($token['type'] === HTML5_Tokenizer::EOF) {
    +            // parse error
    +            /* If the current node is a script  element, mark the script
    +             * element as "already executed". */
    +            // probably not necessary
    +            array_pop($this->stack);
    +            $this->mode = $this->original_mode;
    +            $this->emitToken($token);
    +        } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'script') {
    +            array_pop($this->stack);
    +            $this->mode = $this->original_mode;
    +            // we're ignoring all of the execution stuff
    +        } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG) {
    +            array_pop($this->stack);
    +            $this->mode = $this->original_mode;
    +        }
    +    break;
    +
    +    case self::IN_TABLE:
    +        $clear = array('html', 'table');
    +
    +        /* A character token that is one of one of U+0009 CHARACTER TABULATION,
    +        U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
    +        or U+0020 SPACE */
    +        if($token['type'] === HTML5_Tokenizer::SPACECHARACTER &&
    +        /* If the current table is tainted, then act as described in
    +         * the "anything else" entry below. */
    +        // Note: hsivonen has a test that fails due to this line
    +        // because he wants to convince Hixie not to do taint
    +        !$this->currentTableIsTainted()) {
    +            /* Append the character to the current node. */
    +            $this->insertText($token['data']);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the current node with the data
    +            attribute set to the data given in the comment token. */
    +            $this->insertComment($token['data']);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        /* A start tag whose tag name is "caption" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'caption') {
    +            /* Clear the stack back to a table context. */
    +            $this->clearStackToTableContext($clear);
    +
    +            /* Insert a marker at the end of the list of active
    +            formatting elements. */
    +            $this->a_formatting[] = self::MARKER;
    +
    +            /* Insert an HTML element for the token, then switch the
    +            insertion mode to "in caption". */
    +            $this->insertElement($token);
    +            $this->mode = self::IN_CAPTION;
    +
    +        /* A start tag whose tag name is "colgroup" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'colgroup') {
    +            /* Clear the stack back to a table context. */
    +            $this->clearStackToTableContext($clear);
    +
    +            /* Insert an HTML element for the token, then switch the
    +            insertion mode to "in column group". */
    +            $this->insertElement($token);
    +            $this->mode = self::IN_COLUMN_GROUP;
    +
    +        /* A start tag whose tag name is "col" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'col') {
    +            $this->emitToken(array(
    +                'name' => 'colgroup',
    +                'type' => HTML5_Tokenizer::STARTTAG,
    +                'attr' => array()
    +            ));
    +
    +            $this->emitToken($token);
    +
    +        /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
    +        array('tbody', 'tfoot', 'thead'))) {
    +            /* Clear the stack back to a table context. */
    +            $this->clearStackToTableContext($clear);
    +
    +            /* Insert an HTML element for the token, then switch the insertion
    +            mode to "in table body". */
    +            $this->insertElement($token);
    +            $this->mode = self::IN_TABLE_BODY;
    +
    +        /* A start tag whose tag name is one of: "td", "th", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        in_array($token['name'], array('td', 'th', 'tr'))) {
    +            /* Act as if a start tag token with the tag name "tbody" had been
    +            seen, then reprocess the current token. */
    +            $this->emitToken(array(
    +                'name' => 'tbody',
    +                'type' => HTML5_Tokenizer::STARTTAG,
    +                'attr' => array()
    +            ));
    +
    +            $this->emitToken($token);
    +
    +        /* A start tag whose tag name is "table" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'table') {
    +            /* Parse error. Act as if an end tag token with the tag name "table"
    +            had been seen, then, if that token wasn't ignored, reprocess the
    +            current token. */
    +            $this->emitToken(array(
    +                'name' => 'table',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +
    +            if (!$this->ignored) $this->emitToken($token);
    +
    +        /* An end tag whose tag name is "table" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'table') {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. (fragment case) */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                do {
    +                    $node = array_pop($this->stack);
    +                } while ($node->tagName !== 'table');
    +
    +                /* Reset the insertion mode appropriately. */
    +                $this->resetInsertionMode();
    +            }
    +
    +        /* An end tag whose tag name is one of: "body", "caption", "col",
    +        "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
    +        'tfoot', 'th', 'thead', 'tr'))) {
    +            // Parse error. Ignore the token.
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        ($token['name'] === 'style' || $token['name'] === 'script')) {
    +            $this->processWithRulesFor($token, self::IN_HEAD);
    +
    +        } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'input' &&
    +        // assignment is intentional
    +        /* If the token does not have an attribute with the name "type", or
    +         * if it does, but that attribute's value is not an ASCII
    +         * case-insensitive match for the string "hidden", then: act as
    +         * described in the "anything else" entry below. */
    +        ($type = $this->getAttr($token, 'type')) && strtolower($type) === 'hidden') {
    +            // I.e., if its an input with the type attribute == 'hidden'
    +            /* Otherwise */
    +            // parse error
    +            $this->insertElement($token);
    +            array_pop($this->stack);
    +        } elseif ($token['type'] === HTML5_Tokenizer::EOF) {
    +            /* If the current node is not the root html element, then this is a parse error. */
    +            if (end($this->stack)->tagName !== 'html') {
    +                // Note: It can only be the current node in the fragment case.
    +                // parse error
    +            }
    +            /* Stop parsing. */
    +        /* Anything else */
    +        } else {
    +            /* Parse error. Process the token as if the insertion mode was "in
    +            body", with the following exception: */
    +
    +            $old = $this->foster_parent;
    +            $this->foster_parent = true;
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +            $this->foster_parent = $old;
    +        }
    +    break;
    +
    +    case self::IN_CAPTION:
    +        /* An end tag whose tag name is "caption" */
    +        if($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'caption') {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. (fragment case) */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                $this->ignored = true;
    +                // Ignore
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Generate implied end tags. */
    +                $this->generateImpliedEndTags();
    +
    +                /* Now, if the current node is not a caption element, then this
    +                is a parse error. */
    +                // XERROR: implement
    +
    +                /* Pop elements from this stack until a caption element has
    +                been popped from the stack. */
    +                do {
    +                    $node = array_pop($this->stack);
    +                } while ($node->tagName !== 'caption');
    +
    +                /* Clear the list of active formatting elements up to the last
    +                marker. */
    +                $this->clearTheActiveFormattingElementsUpToTheLastMarker();
    +
    +                /* Switch the insertion mode to "in table". */
    +                $this->mode = self::IN_TABLE;
    +            }
    +
    +        /* A start tag whose tag name is one of: "caption", "col", "colgroup",
    +        "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
    +        name is "table" */
    +        } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
    +        array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
    +        'thead', 'tr'))) || ($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'table')) {
    +            /* Parse error. Act as if an end tag with the tag name "caption"
    +            had been seen, then, if that token wasn't ignored, reprocess the
    +            current token. */
    +            $this->emitToken(array(
    +                'name' => 'caption',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +
    +            if (!$this->ignored) $this->emitToken($token);
    +
    +        /* An end tag whose tag name is one of: "body", "col", "colgroup",
    +        "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
    +        'thead', 'tr'))) {
    +            // Parse error. Ignore the token.
    +            $this->ignored = true;
    +
    +        /* Anything else */
    +        } else {
    +            /* Process the token as if the insertion mode was "in body". */
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +        }
    +    break;
    +
    +    case self::IN_COLUMN_GROUP:
    +        /* A character token that is one of one of U+0009 CHARACTER TABULATION,
    +        U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
    +        or U+0020 SPACE */
    +        if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
    +            /* Append the character to the current node. */
    +            $this->insertText($token['data']);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the current node with the data
    +            attribute set to the data given in the comment token. */
    +            $this->insertToken($token['data']);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* A start tag whose tag name is "col" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'col') {
    +            /* Insert a col element for the token. Immediately pop the current
    +            node off the stack of open elements. */
    +            $this->insertElement($token);
    +            array_pop($this->stack);
    +            // XERROR: Acknowledge the token's self-closing flag, if it is set.
    +
    +        /* An end tag whose tag name is "colgroup" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'colgroup') {
    +            /* If the current node is the root html element, then this is a
    +            parse error, ignore the token. (fragment case) */
    +            if(end($this->stack)->tagName === 'html') {
    +                $this->ignored = true;
    +
    +            /* Otherwise, pop the current node (which will be a colgroup
    +            element) from the stack of open elements. Switch the insertion
    +            mode to "in table". */
    +            } else {
    +                array_pop($this->stack);
    +                $this->mode = self::IN_TABLE;
    +            }
    +
    +        /* An end tag whose tag name is "col" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'col') {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +
    +        /* An end-of-file token */
    +        /* If the current node is the root html  element */
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF && end($this->stack)->tagName === 'html') {
    +            /* Stop parsing */
    +
    +        /* Anything else */
    +        } else {
    +            /* Act as if an end tag with the tag name "colgroup" had been seen,
    +            and then, if that token wasn't ignored, reprocess the current token. */
    +            $this->emitToken(array(
    +                'name' => 'colgroup',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +
    +            if (!$this->ignored) $this->emitToken($token);
    +        }
    +    break;
    +
    +    case self::IN_TABLE_BODY:
    +        $clear = array('tbody', 'tfoot', 'thead', 'html');
    +
    +        /* A start tag whose tag name is "tr" */
    +        if($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'tr') {
    +            /* Clear the stack back to a table body context. */
    +            $this->clearStackToTableContext($clear);
    +
    +            /* Insert a tr element for the token, then switch the insertion
    +            mode to "in row". */
    +            $this->insertElement($token);
    +            $this->mode = self::IN_ROW;
    +
    +        /* A start tag whose tag name is one of: "th", "td" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        ($token['name'] === 'th' ||    $token['name'] === 'td')) {
    +            /* Parse error. Act as if a start tag with the tag name "tr" had
    +            been seen, then reprocess the current token. */
    +            $this->emitToken(array(
    +                'name' => 'tr',
    +                'type' => HTML5_Tokenizer::STARTTAG,
    +                'attr' => array()
    +            ));
    +
    +            $this->emitToken($token);
    +
    +        /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                // Parse error
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Clear the stack back to a table body context. */
    +                $this->clearStackToTableContext($clear);
    +
    +                /* Pop the current node from the stack of open elements. Switch
    +                the insertion mode to "in table". */
    +                array_pop($this->stack);
    +                $this->mode = self::IN_TABLE;
    +            }
    +
    +        /* A start tag whose tag name is one of: "caption", "col", "colgroup",
    +        "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
    +        } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
    +        array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead'))) ||
    +        ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
    +            /* If the stack of open elements does not have a tbody, thead, or
    +            tfoot element in table scope, this is a parse error. Ignore the
    +            token. (fragment case) */
    +            if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
    +                // parse error
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Clear the stack back to a table body context. */
    +                $this->clearStackToTableContext($clear);
    +
    +                /* Act as if an end tag with the same tag name as the current
    +                node ("tbody", "tfoot", or "thead") had been seen, then
    +                reprocess the current token. */
    +                $this->emitToken(array(
    +                    'name' => end($this->stack)->tagName,
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +
    +                $this->emitToken($token);
    +            }
    +
    +        /* An end tag whose tag name is one of: "body", "caption", "col",
    +        "colgroup", "html", "td", "th", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +
    +        /* Anything else */
    +        } else {
    +            /* Process the token as if the insertion mode was "in table". */
    +            $this->processWithRulesFor($token, self::IN_TABLE);
    +        }
    +    break;
    +
    +    case self::IN_ROW:
    +        $clear = array('tr', 'html');
    +
    +        /* A start tag whose tag name is one of: "th", "td" */
    +        if($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        ($token['name'] === 'th' || $token['name'] === 'td')) {
    +            /* Clear the stack back to a table row context. */
    +            $this->clearStackToTableContext($clear);
    +
    +            /* Insert an HTML element for the token, then switch the insertion
    +            mode to "in cell". */
    +            $this->insertElement($token);
    +            $this->mode = self::IN_CELL;
    +
    +            /* Insert a marker at the end of the list of active formatting
    +            elements. */
    +            $this->a_formatting[] = self::MARKER;
    +
    +        /* An end tag whose tag name is "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'tr') {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. (fragment case) */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                // Ignore.
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Clear the stack back to a table row context. */
    +                $this->clearStackToTableContext($clear);
    +
    +                /* Pop the current node (which will be a tr element) from the
    +                stack of open elements. Switch the insertion mode to "in table
    +                body". */
    +                array_pop($this->stack);
    +                $this->mode = self::IN_TABLE_BODY;
    +            }
    +
    +        /* A start tag whose tag name is one of: "caption", "col", "colgroup",
    +        "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
    +        } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
    +        array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) ||
    +        ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
    +            /* Act as if an end tag with the tag name "tr" had been seen, then,
    +            if that token wasn't ignored, reprocess the current token. */
    +            $this->emitToken(array(
    +                'name' => 'tr',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +            if (!$this->ignored) $this->emitToken($token);
    +
    +        /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Otherwise, act as if an end tag with the tag name "tr" had
    +                been seen, then reprocess the current token. */
    +                $this->emitToken(array(
    +                    'name' => 'tr',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +
    +                $this->emitToken($token);
    +            }
    +
    +        /* An end tag whose tag name is one of: "body", "caption", "col",
    +        "colgroup", "html", "td", "th" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th'))) {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +
    +        /* Anything else */
    +        } else {
    +            /* Process the token as if the insertion mode was "in table". */
    +            $this->processWithRulesFor($token, self::IN_TABLE);
    +        }
    +    break;
    +
    +    case self::IN_CELL:
    +        /* An end tag whose tag name is one of: "td", "th" */
    +        if($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        ($token['name'] === 'td' || $token['name'] === 'th')) {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as that of the token, then this is a
    +            parse error and the token must be ignored. */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                $this->ignored = true;
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Generate implied end tags, except for elements with the same
    +                tag name as the token. */
    +                $this->generateImpliedEndTags(array($token['name']));
    +
    +                /* Now, if the current node is not an element with the same tag
    +                name as the token, then this is a parse error. */
    +                // XERROR: Implement parse error code
    +
    +                /* Pop elements from this stack until an element with the same
    +                tag name as the token has been popped from the stack. */
    +                do {
    +                    $node = array_pop($this->stack);
    +                } while ($node->tagName !== $token['name']);
    +
    +                /* Clear the list of active formatting elements up to the last
    +                marker. */
    +                $this->clearTheActiveFormattingElementsUpToTheLastMarker();
    +
    +                /* Switch the insertion mode to "in row". (The current node
    +                will be a tr element at this point.) */
    +                $this->mode = self::IN_ROW;
    +            }
    +
    +        /* A start tag whose tag name is one of: "caption", "col", "colgroup",
    +        "tbody", "td", "tfoot", "th", "thead", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
    +        array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
    +        'thead', 'tr'))) {
    +            /* If the stack of open elements does not have a td or th element
    +            in table scope, then this is a parse error; ignore the token.
    +            (fragment case) */
    +            if(!$this->elementInScope(array('td', 'th'), true)) {
    +                // parse error
    +                $this->ignored = true;
    +
    +            /* Otherwise, close the cell (see below) and reprocess the current
    +            token. */
    +            } else {
    +                $this->closeCell();
    +                $this->emitToken($token);
    +            }
    +
    +        /* An end tag whose tag name is one of: "body", "caption", "col",
    +        "colgroup", "html" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('body', 'caption', 'col', 'colgroup', 'html'))) {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +
    +        /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
    +        "thead", "tr" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
    +        array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
    +            /* If the stack of open elements does not have a td or th element
    +            in table scope, then this is a parse error; ignore the token.
    +            (innerHTML case) */
    +            if(!$this->elementInScope(array('td', 'th'), true)) {
    +                // Parse error
    +                $this->ignored = true;
    +
    +            /* Otherwise, close the cell (see below) and reprocess the current
    +            token. */
    +            } else {
    +                $this->closeCell();
    +                $this->emitToken($token);
    +            }
    +
    +        /* Anything else */
    +        } else {
    +            /* Process the token as if the insertion mode was "in body". */
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +        }
    +    break;
    +
    +    case self::IN_SELECT:
    +        /* Handle the token as follows: */
    +
    +        /* A character token */
    +        if(
    +            $token['type'] === HTML5_Tokenizer::CHARACTER ||
    +            $token['type'] === HTML5_Tokenizer::SPACECHARACTER
    +        ) {
    +            /* Append the token's character to the current node. */
    +            $this->insertText($token['data']);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the current node with the data
    +            attribute set to the data given in the comment token. */
    +            $this->insertComment($token['data']);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
    +            $this->processWithRulesFor($token, self::INBODY);
    +
    +        /* A start tag token whose tag name is "option" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'option') {
    +            /* If the current node is an option element, act as if an end tag
    +            with the tag name "option" had been seen. */
    +            if(end($this->stack)->tagName === 'option') {
    +                $this->emitToken(array(
    +                    'name' => 'option',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +            }
    +
    +            /* Insert an HTML element for the token. */
    +            $this->insertElement($token);
    +
    +        /* A start tag token whose tag name is "optgroup" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'optgroup') {
    +            /* If the current node is an option element, act as if an end tag
    +            with the tag name "option" had been seen. */
    +            if(end($this->stack)->tagName === 'option') {
    +                $this->emitToken(array(
    +                    'name' => 'option',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +            }
    +
    +            /* If the current node is an optgroup element, act as if an end tag
    +            with the tag name "optgroup" had been seen. */
    +            if(end($this->stack)->tagName === 'optgroup') {
    +                $this->emitToken(array(
    +                    'name' => 'optgroup',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +            }
    +
    +            /* Insert an HTML element for the token. */
    +            $this->insertElement($token);
    +
    +        /* An end tag token whose tag name is "optgroup" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'optgroup') {
    +            /* First, if the current node is an option element, and the node
    +            immediately before it in the stack of open elements is an optgroup
    +            element, then act as if an end tag with the tag name "option" had
    +            been seen. */
    +            $elements_in_stack = count($this->stack);
    +
    +            if($this->stack[$elements_in_stack - 1]->tagName === 'option' &&
    +            $this->stack[$elements_in_stack - 2]->tagName === 'optgroup') {
    +                $this->emitToken(array(
    +                    'name' => 'option',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +            }
    +
    +            /* If the current node is an optgroup element, then pop that node
    +            from the stack of open elements. Otherwise, this is a parse error,
    +            ignore the token. */
    +            if(end($this->stack)->tagName === 'optgroup') {
    +                array_pop($this->stack);
    +            } else {
    +                // parse error
    +                $this->ignored = true;
    +            }
    +
    +        /* An end tag token whose tag name is "option" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'option') {
    +            /* If the current node is an option element, then pop that node
    +            from the stack of open elements. Otherwise, this is a parse error,
    +            ignore the token. */
    +            if(end($this->stack)->tagName === 'option') {
    +                array_pop($this->stack);
    +            } else {
    +                // parse error
    +                $this->ignored = true;
    +            }
    +
    +        /* An end tag whose tag name is "select" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'select') {
    +            /* If the stack of open elements does not have an element in table
    +            scope with the same tag name as the token, this is a parse error.
    +            Ignore the token. (fragment case) */
    +            if(!$this->elementInScope($token['name'], true)) {
    +                $this->ignored = true;
    +                // parse error
    +
    +            /* Otherwise: */
    +            } else {
    +                /* Pop elements from the stack of open elements until a select
    +                element has been popped from the stack. */
    +                do {
    +                    $node = array_pop($this->stack);
    +                } while ($node->tagName !== 'select');
    +
    +                /* Reset the insertion mode appropriately. */
    +                $this->resetInsertionMode();
    +            }
    +
    +        /* A start tag whose tag name is "select" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'select') {
    +            /* Parse error. Act as if the token had been an end tag with the
    +            tag name "select" instead. */
    +            $this->emitToken(array(
    +                'name' => 'select',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        ($token['name'] === 'input' || $token['name'] === 'textarea')) {
    +            // parse error
    +            $this->emitToken(array(
    +                'name' => 'select',
    +                'type' => HTML5_Tokenizer::ENDTAG
    +            ));
    +            $this->emitToken($token);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
    +            $this->processWithRulesFor($token, self::IN_HEAD);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            // XERROR: If the current node is not the root html element, then this is a parse error.
    +            /* Stop parsing */
    +
    +        /* Anything else */
    +        } else {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +        }
    +    break;
    +
    +    case self::IN_SELECT_IN_TABLE:
    +
    +        if($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        in_array($token['name'], array('caption', 'table', 'tbody',
    +        'tfoot', 'thead', 'tr', 'td', 'th'))) {
    +            // parse error
    +            $this->emitToken(array(
    +                'name' => 'select',
    +                'type' => HTML5_Tokenizer::ENDTAG,
    +            ));
    +            $this->emitToken($token);
    +
    +        /* An end tag whose tag name is one of: "caption", "table", "tbody",
    +        "tfoot", "thead", "tr", "td", "th" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        in_array($token['name'], array('caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th')))  {
    +            /* Parse error. */
    +            // parse error
    +
    +            /* If the stack of open elements has an element in table scope with
    +            the same tag name as that of the token, then act as if an end tag
    +            with the tag name "select" had been seen, and reprocess the token.
    +            Otherwise, ignore the token. */
    +            if($this->elementInScope($token['name'], true)) {
    +                $this->emitToken(array(
    +                    'name' => 'select',
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +
    +                $this->emitToken($token);
    +            } else {
    +                $this->ignored = true;
    +            }
    +        } else {
    +            $this->processWithRulesFor($token, self::IN_SELECT);
    +        }
    +    break;
    +
    +    case self::IN_FOREIGN_CONTENT:
    +        if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
    +        $token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
    +            $this->insertText($token['data']);
    +        } elseif ($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            $this->insertComment($token['data']);
    +        } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // XERROR: parse error
    +        } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'script' && end($this->stack)->tagName === 'script' &&
    +        end($this->stack)->namespaceURI === self::NS_SVG) {
    +            array_pop($this->stack);
    +            // a bunch of script running mumbo jumbo
    +        } elseif (
    +            ($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +                ((
    +                    $token['name'] !== 'mglyph' &&
    +                    $token['name'] !== 'malignmark' &&
    +                    end($this->stack)->namespaceURI === self::NS_MATHML &&
    +                    in_array(end($this->stack)->tagName, array('mi', 'mo', 'mn', 'ms', 'mtext'))
    +                ) ||
    +                (
    +                    $token['name'] === 'svg' &&
    +                    end($this->stack)->namespaceURI === self::NS_MATHML &&
    +                    end($this->stack)->tagName === 'annotation-xml'
    +                ) ||
    +                (
    +                    end($this->stack)->namespaceURI === self::NS_SVG &&
    +                    in_array(end($this->stack)->tagName, array('foreignObject', 'desc', 'title'))
    +                ) ||
    +                (
    +                    // XSKETCHY
    +                    end($this->stack)->namespaceURI === self::NS_HTML
    +                ))
    +            ) || $token['type'] === HTML5_Tokenizer::ENDTAG
    +        ) {
    +            $this->processWithRulesFor($token, $this->secondary_mode);
    +            /* If, after doing so, the insertion mode is still "in foreign 
    +             * content", but there is no element in scope that has a namespace 
    +             * other than the HTML namespace, switch the insertion mode to the 
    +             * secondary insertion mode. */
    +            if ($this->mode === self::IN_FOREIGN_CONTENT) {
    +                $found = false;
    +                // this basically duplicates elementInScope()
    +                for ($i = count($this->stack) - 1; $i >= 0; $i--) {
    +                    $node = $this->stack[$i];
    +                    if ($node->namespaceURI !== self::NS_HTML) {
    +                        $found = true;
    +                        break;
    +                    } elseif (in_array($node->tagName, array('table', 'html',
    +                    'applet', 'caption', 'td', 'th', 'button', 'marquee',
    +                    'object')) || ($node->tagName === 'foreignObject' &&
    +                    $node->namespaceURI === self::NS_SVG)) {
    +                        break;
    +                    }
    +                }
    +                if (!$found) {
    +                    $this->mode = $this->secondary_mode;
    +                }
    +            }
    +        } elseif ($token['type'] === HTML5_Tokenizer::EOF || (
    +        $token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        (in_array($token['name'], array('b', "big", "blockquote", "body", "br", 
    +        "center", "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", 
    +        "h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing", 
    +        "menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s",  "small", 
    +        "span", "strong", "strike",  "sub", "sup", "table", "tt", "u", "ul", 
    +        "var")) || ($token['name'] === 'font' && ($this->getAttr($token, 'color') ||
    +        $this->getAttr($token, 'face') || $this->getAttr($token, 'size')))))) {
    +            // XERROR: parse error
    +            do {
    +                $node = array_pop($this->stack);
    +            } while ($node->namespaceURI !== self::NS_HTML);
    +            $this->stack[] = $node;
    +            $this->mode = $this->secondary_mode;
    +            $this->emitToken($token);
    +        } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG) {
    +            static $svg_lookup = array(
    +                'altglyph' => 'altGlyph',
    +                'altglyphdef' => 'altGlyphDef',
    +                'altglyphitem' => 'altGlyphItem',
    +                'animatecolor' => 'animateColor',
    +                'animatemotion' => 'animateMotion',
    +                'animatetransform' => 'animateTransform',
    +                'clippath' => 'clipPath',
    +                'feblend' => 'feBlend',
    +                'fecolormatrix' => 'feColorMatrix',
    +                'fecomponenttransfer' => 'feComponentTransfer',
    +                'fecomposite' => 'feComposite',
    +                'feconvolvematrix' => 'feConvolveMatrix',
    +                'fediffuselighting' => 'feDiffuseLighting',
    +                'fedisplacementmap' => 'feDisplacementMap',
    +                'fedistantlight' => 'feDistantLight',
    +                'feflood' => 'feFlood',
    +                'fefunca' => 'feFuncA',
    +                'fefuncb' => 'feFuncB',
    +                'fefuncg' => 'feFuncG',
    +                'fefuncr' => 'feFuncR',
    +                'fegaussianblur' => 'feGaussianBlur',
    +                'feimage' => 'feImage',
    +                'femerge' => 'feMerge',
    +                'femergenode' => 'feMergeNode',
    +                'femorphology' => 'feMorphology',
    +                'feoffset' => 'feOffset',
    +                'fepointlight' => 'fePointLight',
    +                'fespecularlighting' => 'feSpecularLighting',
    +                'fespotlight' => 'feSpotLight',
    +                'fetile' => 'feTile',
    +                'feturbulence' => 'feTurbulence',
    +                'foreignobject' => 'foreignObject',
    +                'glyphref' => 'glyphRef',
    +                'lineargradient' => 'linearGradient',
    +                'radialgradient' => 'radialGradient',
    +                'textpath' => 'textPath',
    +            );
    +            $current = end($this->stack);
    +            if ($current->namespaceURI === self::NS_MATHML) {
    +                $token = $this->adjustMathMLAttributes($token);
    +            }
    +            if ($current->namespaceURI === self::NS_SVG &&
    +            isset($svg_lookup[$token['name']])) {
    +                $token['name'] = $svg_lookup[$token['name']];
    +            }
    +            if ($current->namespaceURI === self::NS_SVG) {
    +                $token = $this->adjustSVGAttributes($token);
    +            }
    +            $token = $this->adjustForeignAttributes($token);
    +            $this->insertForeignElement($token, $current->namespaceURI);
    +            if (isset($token['self-closing'])) {
    +                array_pop($this->stack);
    +                // XERROR: acknowledge self-closing flag
    +            }
    +        }
    +    break;
    +
    +    case self::AFTER_BODY:
    +        /* Handle the token as follows: */
    +
    +        /* A character token that is one of one of U+0009 CHARACTER TABULATION,
    +        U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
    +        or U+0020 SPACE */
    +        if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
    +            /* Process the token as it would be processed if the insertion mode
    +            was "in body". */
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the first element in the stack of open
    +            elements (the html element), with the data attribute set to the
    +            data given in the comment token. */
    +            $comment = $this->dom->createComment($token['data']);
    +            $this->stack[0]->appendChild($comment);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* An end tag with the tag name "html" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'html') {
    +            /*     If the parser was originally created as part of the HTML
    +             *     fragment parsing algorithm, this is a parse error; ignore
    +             *     the token. (fragment case) */
    +            $this->ignored = true;
    +            // XERROR: implement this
    +
    +            $this->mode = self::AFTER_AFTER_BODY;
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            /* Stop parsing */
    +
    +        /* Anything else */
    +        } else {
    +            /* Parse error. Set the insertion mode to "in body" and reprocess
    +            the token. */
    +            $this->mode = self::IN_BODY;
    +            $this->emitToken($token);
    +        }
    +    break;
    +
    +    case self::IN_FRAMESET:
    +        /* Handle the token as follows: */
    +
    +        /* A character token that is one of one of U+0009 CHARACTER TABULATION,
    +        U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
    +        U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
    +        if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
    +            /* Append the character to the current node. */
    +            $this->insertText($token['data']);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the current node with the data
    +            attribute set to the data given in the comment token. */
    +            $this->insertComment($token['data']);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        /* A start tag with the tag name "frameset" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'frameset') {
    +            $this->insertElement($token);
    +
    +        /* An end tag with the tag name "frameset" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'frameset') {
    +            /* If the current node is the root html element, then this is a
    +            parse error; ignore the token. (fragment case) */
    +            if(end($this->stack)->tagName === 'html') {
    +                $this->ignored = true;
    +                // Parse error
    +
    +            } else {
    +                /* Otherwise, pop the current node from the stack of open
    +                elements. */
    +                array_pop($this->stack);
    +
    +                /* If the parser was not originally created as part of the HTML 
    +                 * fragment parsing algorithm  (fragment case), and the current 
    +                 * node is no longer a frameset element, then switch the 
    +                 * insertion mode to "after frameset". */
    +                $this->mode = self::AFTER_FRAMESET;
    +            }
    +
    +        /* A start tag with the tag name "frame" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'frame') {
    +            /* Insert an HTML element for the token. */
    +            $this->insertElement($token);
    +
    +            /* Immediately pop the current node off the stack of open elements. */
    +            array_pop($this->stack);
    +
    +            // XERROR: Acknowledge the token's self-closing flag, if it is set.
    +
    +        /* A start tag with the tag name "noframes" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'noframes') {
    +            /* Process the token using the rules for the "in head" insertion mode. */
    +            $this->processwithRulesFor($token, self::IN_HEAD);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            // XERROR: If the current node is not the root html element, then this is a parse error.
    +            /* Stop parsing */
    +        /* Anything else */
    +        } else {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +        }
    +    break;
    +
    +    case self::AFTER_FRAMESET:
    +        /* Handle the token as follows: */
    +
    +        /* A character token that is one of one of U+0009 CHARACTER TABULATION,
    +        U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
    +        U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
    +        if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
    +            /* Append the character to the current node. */
    +            $this->insertText($token['data']);
    +
    +        /* A comment token */
    +        } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the current node with the data
    +            attribute set to the data given in the comment token. */
    +            $this->insertComment($token['data']);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
    +            // parse error
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* An end tag with the tag name "html" */
    +        } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
    +        $token['name'] === 'html') {
    +            $this->mode = self::AFTER_AFTER_FRAMESET;
    +
    +        /* A start tag with the tag name "noframes" */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
    +        $token['name'] === 'noframes') {
    +            $this->processWithRulesFor($token, self::IN_HEAD);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            /* Stop parsing */
    +
    +        /* Anything else */
    +        } else {
    +            /* Parse error. Ignore the token. */
    +            $this->ignored = true;
    +        }
    +    break;
    +
    +    case self::AFTER_AFTER_BODY:
    +        /* A comment token */
    +        if($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the Document object with the data
    +            attribute set to the data given in the comment token. */
    +            $comment = $this->dom->createComment($token['data']);
    +            $this->dom->appendChild($comment);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
    +        $token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
    +        ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* An end-of-file token */
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            /* OMG DONE!! */
    +        } else {
    +            // parse error
    +            $this->mode = self::IN_BODY;
    +            $this->emitToken($token);
    +        }
    +    break;
    +
    +    case self::AFTER_AFTER_FRAMESET:
    +        /* A comment token */
    +        if($token['type'] === HTML5_Tokenizer::COMMENT) {
    +            /* Append a Comment node to the Document object with the data
    +            attribute set to the data given in the comment token. */
    +            $comment = $this->dom->createComment($token['data']);
    +            $this->dom->appendChild($comment);
    +
    +        } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
    +        $token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
    +        ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
    +            $this->processWithRulesFor($token, self::IN_BODY);
    +
    +        /* An end-of-file token */
    +        } elseif($token['type'] === HTML5_Tokenizer::EOF) {
    +            /* OMG DONE!! */
    +        } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'nofrmaes') {
    +            $this->processWithRulesFor($token, self::IN_HEAD);
    +        } else {
    +            // parse error
    +        }
    +    break;
    +    }
    +        // end funky indenting
    +        }
    +
    +    private function insertElement($token, $append = true) {
    +        $el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
    +
    +        if (!empty($token['attr'])) {
    +            foreach($token['attr'] as $attr) {
    +                if(!$el->hasAttribute($attr['name'])) {
    +                    $el->setAttribute($attr['name'], $attr['value']);
    +                }
    +            }
    +        }
    +        if ($append) {
    +            $this->appendToRealParent($el);
    +            $this->stack[] = $el;
    +        }
    +
    +        return $el;
    +    }
    +
    +    private function insertText($data) {
    +        if ($data === '') return;
    +        if ($this->ignore_lf_token) {
    +            if ($data[0] === "\n") {
    +                $data = substr($data, 1);
    +                if ($data === false) return;
    +            }
    +        }
    +        $text = $this->dom->createTextNode($data);
    +        $this->appendToRealParent($text);
    +    }
    +
    +    private function insertComment($data) {
    +        $comment = $this->dom->createComment($data);
    +        $this->appendToRealParent($comment);
    +    }
    +
    +    private function appendToRealParent($node) {
    +        // this is only for the foster_parent case
    +        /* If the current node is a table, tbody, tfoot, thead, or tr
    +        element, then, whenever a node would be inserted into the current
    +        node, it must instead be inserted into the foster parent element. */
    +        if(!$this->foster_parent || !in_array(end($this->stack)->tagName,
    +        array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
    +            end($this->stack)->appendChild($node);
    +        } else {
    +            $this->fosterParent($node);
    +        }
    +    }
    +
    +    private function elementInScope($el, $table = false) {
    +        if(is_array($el)) {
    +            foreach($el as $element) {
    +                if($this->elementInScope($element, $table)) {
    +                    return true;
    +                }
    +            }
    +
    +            return false;
    +        }
    +
    +        $leng = count($this->stack);
    +
    +        for($n = 0; $n < $leng; $n++) {
    +            /* 1. Initialise node to be the current node (the bottommost node of
    +            the stack). */
    +            $node = $this->stack[$leng - 1 - $n];
    +
    +            if($node->tagName === $el) {
    +                /* 2. If node is the target node, terminate in a match state. */
    +                return true;
    +
    +            // these are the common states for "in scope" and "in table scope"
    +            } elseif($node->tagName === 'table' || $node->tagName === 'html') {
    +                return false;
    +
    +            // these are only valid for "in scope"
    +            } elseif(!$table &&
    +            (in_array($node->tagName, array('applet', 'caption', 'td',
    +                'th', 'button', 'marquee', 'object')) ||
    +                $node->tagName === 'foreignObject' && $node->namespaceURI === self::NS_SVG)) {
    +                return false;
    +            }
    +
    +            /* Otherwise, set node to the previous entry in the stack of open
    +            elements and return to step 2. (This will never fail, since the loop
    +            will always terminate in the previous step if the top of the stack
    +            is reached.) */
    +        }
    +    }
    +
    +    private function reconstructActiveFormattingElements() {
    +        /* 1. If there are no entries in the list of active formatting elements,
    +        then there is nothing to reconstruct; stop this algorithm. */
    +        $formatting_elements = count($this->a_formatting);
    +
    +        if($formatting_elements === 0) {
    +            return false;
    +        }
    +
    +        /* 3. Let entry be the last (most recently added) element in the list
    +        of active formatting elements. */
    +        $entry = end($this->a_formatting);
    +
    +        /* 2. If the last (most recently added) entry in the list of active
    +        formatting elements is a marker, or if it is an element that is in the
    +        stack of open elements, then there is nothing to reconstruct; stop this
    +        algorithm. */
    +        if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
    +            return false;
    +        }
    +
    +        for($a = $formatting_elements - 1; $a >= 0; true) {
    +            /* 4. If there are no entries before entry in the list of active
    +            formatting elements, then jump to step 8. */
    +            if($a === 0) {
    +                $step_seven = false;
    +                break;
    +            }
    +
    +            /* 5. Let entry be the entry one earlier than entry in the list of
    +            active formatting elements. */
    +            $a--;
    +            $entry = $this->a_formatting[$a];
    +
    +            /* 6. If entry is neither a marker nor an element that is also in
    +            thetack of open elements, go to step 4. */
    +            if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
    +                break;
    +            }
    +        }
    +
    +        while(true) {
    +            /* 7. Let entry be the element one later than entry in the list of
    +            active formatting elements. */
    +            if(isset($step_seven) && $step_seven === true) {
    +                $a++;
    +                $entry = $this->a_formatting[$a];
    +            }
    +
    +            /* 8. Perform a shallow clone of the element entry to obtain clone. */
    +            $clone = $entry->cloneNode();
    +
    +            /* 9. Append clone to the current node and push it onto the stack
    +            of open elements  so that it is the new current node. */
    +            $this->appendToRealParent($clone);
    +            $this->stack[] = $clone;
    +
    +            /* 10. Replace the entry for entry in the list with an entry for
    +            clone. */
    +            $this->a_formatting[$a] = $clone;
    +
    +            /* 11. If the entry for clone in the list of active formatting
    +            elements is not the last entry in the list, return to step 7. */
    +            if(end($this->a_formatting) !== $clone) {
    +                $step_seven = true;
    +            } else {
    +                break;
    +            }
    +        }
    +    }
    +
    +    private function clearTheActiveFormattingElementsUpToTheLastMarker() {
    +        /* When the steps below require the UA to clear the list of active
    +        formatting elements up to the last marker, the UA must perform the
    +        following steps: */
    +
    +        while(true) {
    +            /* 1. Let entry be the last (most recently added) entry in the list
    +            of active formatting elements. */
    +            $entry = end($this->a_formatting);
    +
    +            /* 2. Remove entry from the list of active formatting elements. */
    +            array_pop($this->a_formatting);
    +
    +            /* 3. If entry was a marker, then stop the algorithm at this point.
    +            The list has been cleared up to the last marker. */
    +            if($entry === self::MARKER) {
    +                break;
    +            }
    +        }
    +    }
    +
    +    private function generateImpliedEndTags($exclude = array()) {
    +        /* When the steps below require the UA to generate implied end tags,
    +        then, if the current node is a dd element, a dt element, an li element,
    +        a p element, a td element, a th  element, or a tr element, the UA must
    +        act as if an end tag with the respective tag name had been seen and
    +        then generate implied end tags again. */
    +        $node = end($this->stack);
    +        $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
    +
    +        while(in_array(end($this->stack)->tagName, $elements)) {
    +            array_pop($this->stack);
    +        }
    +    }
    +
    +    private function getElementCategory($node) {
    +        if (!is_object($node)) debug_print_backtrace();
    +        $name = $node->tagName;
    +        if(in_array($name, $this->special))
    +            return self::SPECIAL;
    +
    +        elseif(in_array($name, $this->scoping))
    +            return self::SCOPING;
    +
    +        elseif(in_array($name, $this->formatting))
    +            return self::FORMATTING;
    +
    +        else
    +            return self::PHRASING;
    +    }
    +
    +    private function clearStackToTableContext($elements) {
    +        /* When the steps above require the UA to clear the stack back to a
    +        table context, it means that the UA must, while the current node is not
    +        a table element or an html element, pop elements from the stack of open
    +        elements. */
    +        while(true) {
    +            $name = end($this->stack)->tagName;
    +
    +            if(in_array($name, $elements)) {
    +                break;
    +            } else {
    +                array_pop($this->stack);
    +            }
    +        }
    +    }
    +
    +    private function resetInsertionMode($context = null) {
    +        /* 1. Let last be false. */
    +        $last = false;
    +        $leng = count($this->stack);
    +
    +        for($n = $leng - 1; $n >= 0; $n--) {
    +            /* 2. Let node be the last node in the stack of open elements. */
    +            $node = $this->stack[$n];
    +
    +            /* 3. If node is the first node in the stack of open elements, then 
    +             * set last to true and set node to the context  element. (fragment 
    +             * case) */
    +            if($this->stack[0]->isSameNode($node)) {
    +                $last = true;
    +                $node = $context;
    +            }
    +
    +            /* 4. If node is a select element, then switch the insertion mode to
    +            "in select" and abort these steps. (fragment case) */
    +            if($node->tagName === 'select') {
    +                $this->mode = self::IN_SELECT;
    +                break;
    +
    +            /* 5. If node is a td or th element, then switch the insertion mode
    +            to "in cell" and abort these steps. */
    +            } elseif($node->tagName === 'td' || $node->nodeName === 'th') {
    +                $this->mode = self::IN_CELL;
    +                break;
    +
    +            /* 6. If node is a tr element, then switch the insertion mode to
    +            "in    row" and abort these steps. */
    +            } elseif($node->tagName === 'tr') {
    +                $this->mode = self::IN_ROW;
    +                break;
    +
    +            /* 7. If node is a tbody, thead, or tfoot element, then switch the
    +            insertion mode to "in table body" and abort these steps. */
    +            } elseif(in_array($node->tagName, array('tbody', 'thead', 'tfoot'))) {
    +                $this->mode = self::IN_TABLE_BODY;
    +                break;
    +
    +            /* 8. If node is a caption element, then switch the insertion mode
    +            to "in caption" and abort these steps. */
    +            } elseif($node->tagName === 'caption') {
    +                $this->mode = self::IN_CAPTION;
    +                break;
    +
    +            /* 9. If node is a colgroup element, then switch the insertion mode
    +            to "in column group" and abort these steps. (innerHTML case) */
    +            } elseif($node->tagName === 'colgroup') {
    +                $this->mode = self::IN_COLUMN_GROUP;
    +                break;
    +
    +            /* 10. If node is a table element, then switch the insertion mode
    +            to "in table" and abort these steps. */
    +            } elseif($node->tagName === 'table') {
    +                $this->mode = self::IN_TABLE;
    +                break;
    +
    +            /* 11. If node is an element from the MathML namespace or the SVG 
    +             * namespace, then switch the insertion mode to "in foreign 
    +             * content", let the secondary insertion mode be "in body", and 
    +             * abort these steps. */
    +            } elseif($node->namespaceURI === self::NS_SVG ||
    +            $node->namespaceURI === self::NS_MATHML) {
    +                $this->mode = self::IN_FOREIGN_CONTENT;
    +                $this->secondary_mode = self::IN_BODY;
    +                break;
    +
    +            /* 12. If node is a head element, then switch the insertion mode
    +            to "in body" ("in body"! not "in head"!) and abort these steps.
    +            (fragment case) */
    +            } elseif($node->tagName === 'head') {
    +                $this->mode = self::IN_BODY;
    +                break;
    +
    +            /* 13. If node is a body element, then switch the insertion mode to
    +            "in body" and abort these steps. */
    +            } elseif($node->tagName === 'body') {
    +                $this->mode = self::IN_BODY;
    +                break;
    +
    +            /* 14. If node is a frameset element, then switch the insertion
    +            mode to "in frameset" and abort these steps. (fragment case) */
    +            } elseif($node->tagName === 'frameset') {
    +                $this->mode = self::IN_FRAMESET;
    +                break;
    +
    +            /* 15. If node is an html element, then: if the head element
    +            pointer is null, switch the insertion mode to "before head",
    +            otherwise, switch the insertion mode to "after head". In either
    +            case, abort these steps. (fragment case) */
    +            } elseif($node->tagName === 'html') {
    +                $this->mode = ($this->head_pointer === null)
    +                    ? self::BEFORE_HEAD
    +                    : self::AFTER_HEAD;
    +
    +                break;
    +
    +            /* 16. If last is true, then set the insertion mode to "in body"
    +            and    abort these steps. (fragment case) */
    +            } elseif($last) {
    +                $this->mode = self::IN_BODY;
    +                break;
    +            }
    +        }
    +    }
    +
    +    private function closeCell() {
    +        /* If the stack of open elements has a td or th element in table scope,
    +        then act as if an end tag token with that tag name had been seen. */
    +        foreach(array('td', 'th') as $cell) {
    +            if($this->elementInScope($cell, true)) {
    +                $this->emitToken(array(
    +                    'name' => $cell,
    +                    'type' => HTML5_Tokenizer::ENDTAG
    +                ));
    +
    +                break;
    +            }
    +        }
    +    }
    +
    +    private function processWithRulesFor($token, $mode) {
    +        /* "using the rules for the m insertion mode", where m is one of these
    +         * modes, the user agent must use the rules described under the m
    +         * insertion mode's section, but must leave the insertion mode
    +         * unchanged unless the rules in m themselves switch the insertion mode
    +         * to a new value. */
    +        return $this->emitToken($token, $mode);
    +    }
    +
    +    private function insertCDATAElement($token) {
    +        $this->insertElement($token);
    +        $this->original_mode = $this->mode;
    +        $this->mode = self::IN_CDATA_RCDATA;
    +        $this->content_model = HTML5_Tokenizer::CDATA;
    +    }
    +
    +    private function insertRCDATAElement($token) {
    +        $this->insertElement($token);
    +        $this->original_mode = $this->mode;
    +        $this->mode = self::IN_CDATA_RCDATA;
    +        $this->content_model = HTML5_Tokenizer::RCDATA;
    +    }
    +
    +    private function getAttr($token, $key) {
    +        if (!isset($token['attr'])) return false;
    +        $ret = false;
    +        foreach ($token['attr'] as $keypair) {
    +            if ($keypair['name'] === $key) $ret = $keypair['value'];
    +        }
    +        return $ret;
    +    }
    +
    +    private function getCurrentTable() {
    +        /* The current table is the last table  element in the stack of open 
    +         * elements, if there is one. If there is no table element in the stack 
    +         * of open elements (fragment case), then the current table is the 
    +         * first element in the stack of open elements (the html element). */
    +        for ($i = count($this->stack) - 1; $i >= 0; $i--) {
    +            if ($this->stack[$i]->tagName === 'table') {
    +                return $this->stack[$i];
    +            }
    +        }
    +        return $this->stack[0];
    +    }
    +
    +    private function getFosterParent() {
    +        /* The foster parent element is the parent element of the last
    +        table element in the stack of open elements, if there is a
    +        table element and it has such a parent element. If there is no
    +        table element in the stack of open elements (innerHTML case),
    +        then the foster parent element is the first element in the
    +        stack of open elements (the html  element). Otherwise, if there
    +        is a table element in the stack of open elements, but the last
    +        table element in the stack of open elements has no parent, or
    +        its parent node is not an element, then the foster parent
    +        element is the element before the last table element in the
    +        stack of open elements. */
    +        for($n = count($this->stack) - 1; $n >= 0; $n--) {
    +            if($this->stack[$n]->tagName === 'table') {
    +                $table = $this->stack[$n];
    +                break;
    +            }
    +        }
    +
    +        if(isset($table) && $table->parentNode !== null) {
    +            return $table->parentNode;
    +
    +        } elseif(!isset($table)) {
    +            return $this->stack[0];
    +
    +        } elseif(isset($table) && ($table->parentNode === null ||
    +        $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
    +            return $this->stack[$n - 1];
    +        }
    +    }
    +
    +    public function fosterParent($node) {
    +        $foster_parent = $this->getFosterParent();
    +        $table = $this->getCurrentTable(); // almost equivalent to last table element, except it can be html
    +        /* When a node node is to be foster parented, the node node  must be 
    +         * inserted into the foster parent element, and the current table must 
    +         * be marked as tainted. (Once the current table has been tainted, 
    +         * whitespace characters are inserted into the foster parent element 
    +         * instead of the current node.) */
    +        $table->tainted = true;
    +        /* If the foster parent element is the parent element of the last table 
    +         * element in the stack of open elements, then node must be inserted 
    +         * immediately before the last table element in the stack of open 
    +         * elements in the foster parent element; otherwise, node must be 
    +         * appended to the foster parent element. */
    +        if ($table->tagName === 'table' && $table->parentNode->isSameNode($foster_parent)) {
    +            $foster_parent->insertBefore($node, $table);
    +        } else {
    +            $foster_parent->appendChild($node);
    +        }
    +    }
    +
    +    /**
    +     * For debugging, prints the stack
    +     */
    +    private function printStack() {
    +        $names = array();
    +        foreach ($this->stack as $i => $element) {
    +            $names[] = $element->tagName;
    +        }
    +        echo "  -> stack [" . implode(', ', $names) . "]\n";
    +    }
    +
    +    /**
    +     * For debugging, prints active formatting elements
    +     */
    +    private function printActiveFormattingElements() {
    +        if (!$this->a_formatting) return;
    +        $names = array();
    +        foreach ($this->a_formatting as $node) {
    +            if ($node === self::MARKER) $names[] = 'MARKER';
    +            else $names[] = $node->tagName;
    +        }
    +        echo "  -> active formatting [" . implode(', ', $names) . "]\n";
    +    }
    +
    +    public function currentTableIsTainted() {
    +        return !empty($this->getCurrentTable()->tainted);
    +    }
    +
    +    /**
    +     * Sets up the tree constructor for building a fragment.
    +     */
    +    public function setupContext($context = null) {
    +        $this->fragment = true;
    +        if ($context) {
    +            $context = $this->dom->createElementNS(self::NS_HTML, $context);
    +            /* 4.1. Set the HTML parser's tokenization  stage's content model
    +             * flag according to the context element, as follows: */
    +            switch ($context->tagName) {
    +            case 'title': case 'textarea':
    +                $this->content_model = HTML5_Tokenizer::RCDATA;
    +                break;
    +            case 'style': case 'script': case 'xmp': case 'iframe':
    +            case 'noembed': case 'noframes':
    +                $this->content_model = HTML5_Tokenizer::CDATA;
    +                break;
    +            case 'noscript':
    +                // XSCRIPT: assuming scripting is enabled
    +                $this->content_model = HTML5_Tokenizer::CDATA;
    +                break;
    +            case 'plaintext':
    +                $this->content_model = HTML5_Tokenizer::PLAINTEXT;
    +                break;
    +            }
    +            /* 4.2. Let root be a new html element with no attributes. */
    +            $root = $this->dom->createElementNS(self::NS_HTML, 'html');
    +            $this->root = $root;
    +            /* 4.3 Append the element root to the Document node created above. */
    +            $this->dom->appendChild($root);
    +            /* 4.4 Set up the parser's stack of open elements so that it 
    +             * contains just the single element root. */
    +            $this->stack = array($root);
    +            /* 4.5 Reset the parser's insertion mode appropriately. */
    +            $this->resetInsertionMode($context);
    +            /* 4.6 Set the parser's form element pointer  to the nearest node 
    +             * to the context element that is a form element (going straight up 
    +             * the ancestor chain, and including the element itself, if it is a 
    +             * form element), or, if there is no such form element, to null. */
    +            $node = $context;
    +            do {
    +                if ($node->tagName === 'form') {
    +                    $this->form_pointer = $node;
    +                    break;
    +                }
    +            } while ($node = $node->parentNode);
    +        }
    +    }
    +
    +    public function adjustMathMLAttributes($token) {
    +        foreach ($token['attr'] as &$kp) {
    +            if ($kp['name'] === 'definitionurl') {
    +                $kp['name'] = 'definitionURL';
    +            }
    +        }
    +        return $token;
    +    }
    +
    +    public function adjustSVGAttributes($token) {
    +        static $lookup = array(
    +            'attributename' => 'attributeName',
    +            'attributetype' => 'attributeType',
    +            'basefrequency' => 'baseFrequency',
    +            'baseprofile' => 'baseProfile',
    +            'calcmode' => 'calcMode',
    +            'clippathunits' => 'clipPathUnits',
    +            'contentscripttype' => 'contentScriptType',
    +            'contentstyletype' => 'contentStyleType',
    +            'diffuseconstant' => 'diffuseConstant',
    +            'edgemode' => 'edgeMode',
    +            'externalresourcesrequired' => 'externalResourcesRequired',
    +            'filterres' => 'filterRes',
    +            'filterunits' => 'filterUnits',
    +            'glyphref' => 'glyphRef',
    +            'gradienttransform' => 'gradientTransform',
    +            'gradientunits' => 'gradientUnits',
    +            'kernelmatrix' => 'kernelMatrix',
    +            'kernelunitlength' => 'kernelUnitLength',
    +            'keypoints' => 'keyPoints',
    +            'keysplines' => 'keySplines',
    +            'keytimes' => 'keyTimes',
    +            'lengthadjust' => 'lengthAdjust',
    +            'limitingconeangle' => 'limitingConeAngle',
    +            'markerheight' => 'markerHeight',
    +            'markerunits' => 'markerUnits',
    +            'markerwidth' => 'markerWidth',
    +            'maskcontentunits' => 'maskContentUnits',
    +            'maskunits' => 'maskUnits',
    +            'numoctaves' => 'numOctaves',
    +            'pathlength' => 'pathLength',
    +            'patterncontentunits' => 'patternContentUnits',
    +            'patterntransform' => 'patternTransform',
    +            'patternunits' => 'patternUnits',
    +            'pointsatx' => 'pointsAtX',
    +            'pointsaty' => 'pointsAtY',
    +            'pointsatz' => 'pointsAtZ',
    +            'preservealpha' => 'preserveAlpha',
    +            'preserveaspectratio' => 'preserveAspectRatio',
    +            'primitiveunits' => 'primitiveUnits',
    +            'refx' => 'refX',
    +            'refy' => 'refY',
    +            'repeatcount' => 'repeatCount',
    +            'repeatdur' => 'repeatDur',
    +            'requiredextensions' => 'requiredExtensions',
    +            'requiredfeatures' => 'requiredFeatures',
    +            'specularconstant' => 'specularConstant',
    +            'specularexponent' => 'specularExponent',
    +            'spreadmethod' => 'spreadMethod',
    +            'startoffset' => 'startOffset',
    +            'stddeviation' => 'stdDeviation',
    +            'stitchtiles' => 'stitchTiles',
    +            'surfacescale' => 'surfaceScale',
    +            'systemlanguage' => 'systemLanguage',
    +            'tablevalues' => 'tableValues',
    +            'targetx' => 'targetX',
    +            'targety' => 'targetY',
    +            'textlength' => 'textLength',
    +            'viewbox' => 'viewBox',
    +            'viewtarget' => 'viewTarget',
    +            'xchannelselector' => 'xChannelSelector',
    +            'ychannelselector' => 'yChannelSelector',
    +            'zoomandpan' => 'zoomAndPan',
    +        );
    +        foreach ($token['attr'] as &$kp) {
    +            if (isset($lookup[$kp['name']])) {
    +                $kp['name'] = $lookup[$kp['name']];
    +            }
    +        }
    +        return $token;
    +    }
    +
    +    public function adjustForeignAttributes($token) {
    +        static $lookup = array(
    +            'xlink:actuate' => array('xlink', 'actuate', self::NS_XLINK),
    +            'xlink:arcrole' => array('xlink', 'arcrole', self::NS_XLINK),
    +            'xlink:href' => array('xlink', 'href', self::NS_XLINK),
    +            'xlink:role' => array('xlink', 'role', self::NS_XLINK),
    +            'xlink:show' => array('xlink', 'show', self::NS_XLINK),
    +            'xlink:title' => array('xlink', 'title', self::NS_XLINK),
    +            'xlink:type' => array('xlink', 'type', self::NS_XLINK),
    +            'xml:base' => array('xml', 'base', self::NS_XML),
    +            'xml:lang' => array('xml', 'lang', self::NS_XML),
    +            'xml:space' => array('xml', 'space', self::NS_XML),
    +            'xmlns' => array(null, 'xmlns', self::NS_XMLNS),
    +            'xmlns:xlink' => array('xmlns', 'xlink', self::NS_XMLNS),
    +        );
    +        foreach ($token['attr'] as &$kp) {
    +            if (isset($lookup[$kp['name']])) {
    +                $kp['name'] = $lookup[$kp['name']];
    +            }
    +        }
    +        return $token;
    +    }
    +
    +    public function insertForeignElement($token, $namespaceURI) {
    +        $el = $this->dom->createElementNS($namespaceURI, $token['name']);
    +        if (!empty($token['attr'])) {
    +            foreach ($token['attr'] as $kp) {
    +                $attr = $kp['name'];
    +                if (is_array($attr)) {
    +                    $ns = $attr[2];
    +                    $attr = $attr[1];
    +                } else {
    +                    $ns = self::NS_HTML;
    +                }
    +                if (!$el->hasAttributeNS($ns, $attr)) {
    +                    // XSKETCHY: work around godawful libxml bug
    +                    if ($ns === self::NS_XLINK) {
    +                        $el->setAttribute('xlink:'.$attr, $kp['value']);
    +                    } elseif ($ns === self::NS_HTML) {
    +                        // Another godawful libxml bug
    +                        $el->setAttribute($attr, $kp['value']);
    +                    } else {
    +                        $el->setAttributeNS($ns, $attr, $kp['value']);
    +                    }
    +                }
    +            }
    +        }
    +        $this->appendToRealParent($el);
    +        $this->stack[] = $el;
    +        // XERROR: see below
    +        /* If the newly created element has an xmlns attribute in the XMLNS 
    +         * namespace  whose value is not exactly the same as the element's 
    +         * namespace, that is a parse error. Similarly, if the newly created 
    +         * element has an xmlns:xlink attribute in the XMLNS namespace whose 
    +         * value is not the XLink Namespace, that is a parse error. */
    +    }
    +
    +    public function save() {
    +        $this->dom->normalize();
    +        if (!$this->fragment) {
    +            return $this->dom;
    +        } else {
    +            if ($this->root) {
    +                return $this->root->childNodes;
    +            } else {
    +                return $this->dom->childNodes;
    +            }
    +        }
    +    }
    +}
    +
    diff --git a/library/HTML5/named-character-references.ser b/library/HTML5/named-character-references.ser
    new file mode 100644
    index 0000000..3004c4b
    --- /dev/null
    +++ b/library/HTML5/named-character-references.ser
    @@ -0,0 +1 @@
    +a:2137:{s:6:"AElig;";i:198;s:5:"AElig";i:198;s:4:"AMP;";i:38;s:3:"AMP";i:38;s:7:"Aacute;";i:193;s:6:"Aacute";i:193;s:7:"Abreve;";i:258;s:6:"Acirc;";i:194;s:5:"Acirc";i:194;s:4:"Acy;";i:1040;s:4:"Afr;";i:120068;s:7:"Agrave;";i:192;s:6:"Agrave";i:192;s:6:"Alpha;";i:913;s:6:"Amacr;";i:256;s:4:"And;";i:10835;s:6:"Aogon;";i:260;s:5:"Aopf;";i:120120;s:14:"ApplyFunction;";i:8289;s:6:"Aring;";i:197;s:5:"Aring";i:197;s:5:"Ascr;";i:119964;s:7:"Assign;";i:8788;s:7:"Atilde;";i:195;s:6:"Atilde";i:195;s:5:"Auml;";i:196;s:4:"Auml";i:196;s:10:"Backslash;";i:8726;s:5:"Barv;";i:10983;s:7:"Barwed;";i:8966;s:4:"Bcy;";i:1041;s:8:"Because;";i:8757;s:11:"Bernoullis;";i:8492;s:5:"Beta;";i:914;s:4:"Bfr;";i:120069;s:5:"Bopf;";i:120121;s:6:"Breve;";i:728;s:5:"Bscr;";i:8492;s:7:"Bumpeq;";i:8782;s:5:"CHcy;";i:1063;s:5:"COPY;";i:169;s:4:"COPY";i:169;s:7:"Cacute;";i:262;s:4:"Cap;";i:8914;s:21:"CapitalDifferentialD;";i:8517;s:8:"Cayleys;";i:8493;s:7:"Ccaron;";i:268;s:7:"Ccedil;";i:199;s:6:"Ccedil";i:199;s:6:"Ccirc;";i:264;s:8:"Cconint;";i:8752;s:5:"Cdot;";i:266;s:8:"Cedilla;";i:184;s:10:"CenterDot;";i:183;s:4:"Cfr;";i:8493;s:4:"Chi;";i:935;s:10:"CircleDot;";i:8857;s:12:"CircleMinus;";i:8854;s:11:"CirclePlus;";i:8853;s:12:"CircleTimes;";i:8855;s:25:"ClockwiseContourIntegral;";i:8754;s:22:"CloseCurlyDoubleQuote;";i:8221;s:16:"CloseCurlyQuote;";i:8217;s:6:"Colon;";i:8759;s:7:"Colone;";i:10868;s:10:"Congruent;";i:8801;s:7:"Conint;";i:8751;s:16:"ContourIntegral;";i:8750;s:5:"Copf;";i:8450;s:10:"Coproduct;";i:8720;s:32:"CounterClockwiseContourIntegral;";i:8755;s:6:"Cross;";i:10799;s:5:"Cscr;";i:119966;s:4:"Cup;";i:8915;s:7:"CupCap;";i:8781;s:3:"DD;";i:8517;s:9:"DDotrahd;";i:10513;s:5:"DJcy;";i:1026;s:5:"DScy;";i:1029;s:5:"DZcy;";i:1039;s:7:"Dagger;";i:8225;s:5:"Darr;";i:8609;s:6:"Dashv;";i:10980;s:7:"Dcaron;";i:270;s:4:"Dcy;";i:1044;s:4:"Del;";i:8711;s:6:"Delta;";i:916;s:4:"Dfr;";i:120071;s:17:"DiacriticalAcute;";i:180;s:15:"DiacriticalDot;";i:729;s:23:"DiacriticalDoubleAcute;";i:733;s:17:"DiacriticalGrave;";i:96;s:17:"DiacriticalTilde;";i:732;s:8:"Diamond;";i:8900;s:14:"DifferentialD;";i:8518;s:5:"Dopf;";i:120123;s:4:"Dot;";i:168;s:7:"DotDot;";i:8412;s:9:"DotEqual;";i:8784;s:22:"DoubleContourIntegral;";i:8751;s:10:"DoubleDot;";i:168;s:16:"DoubleDownArrow;";i:8659;s:16:"DoubleLeftArrow;";i:8656;s:21:"DoubleLeftRightArrow;";i:8660;s:14:"DoubleLeftTee;";i:10980;s:20:"DoubleLongLeftArrow;";i:10232;s:25:"DoubleLongLeftRightArrow;";i:10234;s:21:"DoubleLongRightArrow;";i:10233;s:17:"DoubleRightArrow;";i:8658;s:15:"DoubleRightTee;";i:8872;s:14:"DoubleUpArrow;";i:8657;s:18:"DoubleUpDownArrow;";i:8661;s:18:"DoubleVerticalBar;";i:8741;s:10:"DownArrow;";i:8595;s:13:"DownArrowBar;";i:10515;s:17:"DownArrowUpArrow;";i:8693;s:10:"DownBreve;";i:785;s:20:"DownLeftRightVector;";i:10576;s:18:"DownLeftTeeVector;";i:10590;s:15:"DownLeftVector;";i:8637;s:18:"DownLeftVectorBar;";i:10582;s:19:"DownRightTeeVector;";i:10591;s:16:"DownRightVector;";i:8641;s:19:"DownRightVectorBar;";i:10583;s:8:"DownTee;";i:8868;s:13:"DownTeeArrow;";i:8615;s:10:"Downarrow;";i:8659;s:5:"Dscr;";i:119967;s:7:"Dstrok;";i:272;s:4:"ENG;";i:330;s:4:"ETH;";i:208;s:3:"ETH";i:208;s:7:"Eacute;";i:201;s:6:"Eacute";i:201;s:7:"Ecaron;";i:282;s:6:"Ecirc;";i:202;s:5:"Ecirc";i:202;s:4:"Ecy;";i:1069;s:5:"Edot;";i:278;s:4:"Efr;";i:120072;s:7:"Egrave;";i:200;s:6:"Egrave";i:200;s:8:"Element;";i:8712;s:6:"Emacr;";i:274;s:17:"EmptySmallSquare;";i:9723;s:21:"EmptyVerySmallSquare;";i:9643;s:6:"Eogon;";i:280;s:5:"Eopf;";i:120124;s:8:"Epsilon;";i:917;s:6:"Equal;";i:10869;s:11:"EqualTilde;";i:8770;s:12:"Equilibrium;";i:8652;s:5:"Escr;";i:8496;s:5:"Esim;";i:10867;s:4:"Eta;";i:919;s:5:"Euml;";i:203;s:4:"Euml";i:203;s:7:"Exists;";i:8707;s:13:"ExponentialE;";i:8519;s:4:"Fcy;";i:1060;s:4:"Ffr;";i:120073;s:18:"FilledSmallSquare;";i:9724;s:22:"FilledVerySmallSquare;";i:9642;s:5:"Fopf;";i:120125;s:7:"ForAll;";i:8704;s:11:"Fouriertrf;";i:8497;s:5:"Fscr;";i:8497;s:5:"GJcy;";i:1027;s:3:"GT;";i:62;s:2:"GT";i:62;s:6:"Gamma;";i:915;s:7:"Gammad;";i:988;s:7:"Gbreve;";i:286;s:7:"Gcedil;";i:290;s:6:"Gcirc;";i:284;s:4:"Gcy;";i:1043;s:5:"Gdot;";i:288;s:4:"Gfr;";i:120074;s:3:"Gg;";i:8921;s:5:"Gopf;";i:120126;s:13:"GreaterEqual;";i:8805;s:17:"GreaterEqualLess;";i:8923;s:17:"GreaterFullEqual;";i:8807;s:15:"GreaterGreater;";i:10914;s:12:"GreaterLess;";i:8823;s:18:"GreaterSlantEqual;";i:10878;s:13:"GreaterTilde;";i:8819;s:5:"Gscr;";i:119970;s:3:"Gt;";i:8811;s:7:"HARDcy;";i:1066;s:6:"Hacek;";i:711;s:4:"Hat;";i:94;s:6:"Hcirc;";i:292;s:4:"Hfr;";i:8460;s:13:"HilbertSpace;";i:8459;s:5:"Hopf;";i:8461;s:15:"HorizontalLine;";i:9472;s:5:"Hscr;";i:8459;s:7:"Hstrok;";i:294;s:13:"HumpDownHump;";i:8782;s:10:"HumpEqual;";i:8783;s:5:"IEcy;";i:1045;s:6:"IJlig;";i:306;s:5:"IOcy;";i:1025;s:7:"Iacute;";i:205;s:6:"Iacute";i:205;s:6:"Icirc;";i:206;s:5:"Icirc";i:206;s:4:"Icy;";i:1048;s:5:"Idot;";i:304;s:4:"Ifr;";i:8465;s:7:"Igrave;";i:204;s:6:"Igrave";i:204;s:3:"Im;";i:8465;s:6:"Imacr;";i:298;s:11:"ImaginaryI;";i:8520;s:8:"Implies;";i:8658;s:4:"Int;";i:8748;s:9:"Integral;";i:8747;s:13:"Intersection;";i:8898;s:15:"InvisibleComma;";i:8291;s:15:"InvisibleTimes;";i:8290;s:6:"Iogon;";i:302;s:5:"Iopf;";i:120128;s:5:"Iota;";i:921;s:5:"Iscr;";i:8464;s:7:"Itilde;";i:296;s:6:"Iukcy;";i:1030;s:5:"Iuml;";i:207;s:4:"Iuml";i:207;s:6:"Jcirc;";i:308;s:4:"Jcy;";i:1049;s:4:"Jfr;";i:120077;s:5:"Jopf;";i:120129;s:5:"Jscr;";i:119973;s:7:"Jsercy;";i:1032;s:6:"Jukcy;";i:1028;s:5:"KHcy;";i:1061;s:5:"KJcy;";i:1036;s:6:"Kappa;";i:922;s:7:"Kcedil;";i:310;s:4:"Kcy;";i:1050;s:4:"Kfr;";i:120078;s:5:"Kopf;";i:120130;s:5:"Kscr;";i:119974;s:5:"LJcy;";i:1033;s:3:"LT;";i:60;s:2:"LT";i:60;s:7:"Lacute;";i:313;s:7:"Lambda;";i:923;s:5:"Lang;";i:10218;s:11:"Laplacetrf;";i:8466;s:5:"Larr;";i:8606;s:7:"Lcaron;";i:317;s:7:"Lcedil;";i:315;s:4:"Lcy;";i:1051;s:17:"LeftAngleBracket;";i:10216;s:10:"LeftArrow;";i:8592;s:13:"LeftArrowBar;";i:8676;s:20:"LeftArrowRightArrow;";i:8646;s:12:"LeftCeiling;";i:8968;s:18:"LeftDoubleBracket;";i:10214;s:18:"LeftDownTeeVector;";i:10593;s:15:"LeftDownVector;";i:8643;s:18:"LeftDownVectorBar;";i:10585;s:10:"LeftFloor;";i:8970;s:15:"LeftRightArrow;";i:8596;s:16:"LeftRightVector;";i:10574;s:8:"LeftTee;";i:8867;s:13:"LeftTeeArrow;";i:8612;s:14:"LeftTeeVector;";i:10586;s:13:"LeftTriangle;";i:8882;s:16:"LeftTriangleBar;";i:10703;s:18:"LeftTriangleEqual;";i:8884;s:17:"LeftUpDownVector;";i:10577;s:16:"LeftUpTeeVector;";i:10592;s:13:"LeftUpVector;";i:8639;s:16:"LeftUpVectorBar;";i:10584;s:11:"LeftVector;";i:8636;s:14:"LeftVectorBar;";i:10578;s:10:"Leftarrow;";i:8656;s:15:"Leftrightarrow;";i:8660;s:17:"LessEqualGreater;";i:8922;s:14:"LessFullEqual;";i:8806;s:12:"LessGreater;";i:8822;s:9:"LessLess;";i:10913;s:15:"LessSlantEqual;";i:10877;s:10:"LessTilde;";i:8818;s:4:"Lfr;";i:120079;s:3:"Ll;";i:8920;s:11:"Lleftarrow;";i:8666;s:7:"Lmidot;";i:319;s:14:"LongLeftArrow;";i:10229;s:19:"LongLeftRightArrow;";i:10231;s:15:"LongRightArrow;";i:10230;s:14:"Longleftarrow;";i:10232;s:19:"Longleftrightarrow;";i:10234;s:15:"Longrightarrow;";i:10233;s:5:"Lopf;";i:120131;s:15:"LowerLeftArrow;";i:8601;s:16:"LowerRightArrow;";i:8600;s:5:"Lscr;";i:8466;s:4:"Lsh;";i:8624;s:7:"Lstrok;";i:321;s:3:"Lt;";i:8810;s:4:"Map;";i:10501;s:4:"Mcy;";i:1052;s:12:"MediumSpace;";i:8287;s:10:"Mellintrf;";i:8499;s:4:"Mfr;";i:120080;s:10:"MinusPlus;";i:8723;s:5:"Mopf;";i:120132;s:5:"Mscr;";i:8499;s:3:"Mu;";i:924;s:5:"NJcy;";i:1034;s:7:"Nacute;";i:323;s:7:"Ncaron;";i:327;s:7:"Ncedil;";i:325;s:4:"Ncy;";i:1053;s:20:"NegativeMediumSpace;";i:8203;s:19:"NegativeThickSpace;";i:8203;s:18:"NegativeThinSpace;";i:8203;s:22:"NegativeVeryThinSpace;";i:8203;s:21:"NestedGreaterGreater;";i:8811;s:15:"NestedLessLess;";i:8810;s:8:"NewLine;";i:10;s:4:"Nfr;";i:120081;s:8:"NoBreak;";i:8288;s:17:"NonBreakingSpace;";i:160;s:5:"Nopf;";i:8469;s:4:"Not;";i:10988;s:13:"NotCongruent;";i:8802;s:10:"NotCupCap;";i:8813;s:21:"NotDoubleVerticalBar;";i:8742;s:11:"NotElement;";i:8713;s:9:"NotEqual;";i:8800;s:10:"NotExists;";i:8708;s:11:"NotGreater;";i:8815;s:16:"NotGreaterEqual;";i:8817;s:15:"NotGreaterLess;";i:8825;s:16:"NotGreaterTilde;";i:8821;s:16:"NotLeftTriangle;";i:8938;s:21:"NotLeftTriangleEqual;";i:8940;s:8:"NotLess;";i:8814;s:13:"NotLessEqual;";i:8816;s:15:"NotLessGreater;";i:8824;s:13:"NotLessTilde;";i:8820;s:12:"NotPrecedes;";i:8832;s:22:"NotPrecedesSlantEqual;";i:8928;s:18:"NotReverseElement;";i:8716;s:17:"NotRightTriangle;";i:8939;s:22:"NotRightTriangleEqual;";i:8941;s:21:"NotSquareSubsetEqual;";i:8930;s:23:"NotSquareSupersetEqual;";i:8931;s:15:"NotSubsetEqual;";i:8840;s:12:"NotSucceeds;";i:8833;s:22:"NotSucceedsSlantEqual;";i:8929;s:17:"NotSupersetEqual;";i:8841;s:9:"NotTilde;";i:8769;s:14:"NotTildeEqual;";i:8772;s:18:"NotTildeFullEqual;";i:8775;s:14:"NotTildeTilde;";i:8777;s:15:"NotVerticalBar;";i:8740;s:5:"Nscr;";i:119977;s:7:"Ntilde;";i:209;s:6:"Ntilde";i:209;s:3:"Nu;";i:925;s:6:"OElig;";i:338;s:7:"Oacute;";i:211;s:6:"Oacute";i:211;s:6:"Ocirc;";i:212;s:5:"Ocirc";i:212;s:4:"Ocy;";i:1054;s:7:"Odblac;";i:336;s:4:"Ofr;";i:120082;s:7:"Ograve;";i:210;s:6:"Ograve";i:210;s:6:"Omacr;";i:332;s:6:"Omega;";i:937;s:8:"Omicron;";i:927;s:5:"Oopf;";i:120134;s:21:"OpenCurlyDoubleQuote;";i:8220;s:15:"OpenCurlyQuote;";i:8216;s:3:"Or;";i:10836;s:5:"Oscr;";i:119978;s:7:"Oslash;";i:216;s:6:"Oslash";i:216;s:7:"Otilde;";i:213;s:6:"Otilde";i:213;s:7:"Otimes;";i:10807;s:5:"Ouml;";i:214;s:4:"Ouml";i:214;s:8:"OverBar;";i:175;s:10:"OverBrace;";i:9182;s:12:"OverBracket;";i:9140;s:16:"OverParenthesis;";i:9180;s:9:"PartialD;";i:8706;s:4:"Pcy;";i:1055;s:4:"Pfr;";i:120083;s:4:"Phi;";i:934;s:3:"Pi;";i:928;s:10:"PlusMinus;";i:177;s:14:"Poincareplane;";i:8460;s:5:"Popf;";i:8473;s:3:"Pr;";i:10939;s:9:"Precedes;";i:8826;s:14:"PrecedesEqual;";i:10927;s:19:"PrecedesSlantEqual;";i:8828;s:14:"PrecedesTilde;";i:8830;s:6:"Prime;";i:8243;s:8:"Product;";i:8719;s:11:"Proportion;";i:8759;s:13:"Proportional;";i:8733;s:5:"Pscr;";i:119979;s:4:"Psi;";i:936;s:5:"QUOT;";i:34;s:4:"QUOT";i:34;s:4:"Qfr;";i:120084;s:5:"Qopf;";i:8474;s:5:"Qscr;";i:119980;s:6:"RBarr;";i:10512;s:4:"REG;";i:174;s:3:"REG";i:174;s:7:"Racute;";i:340;s:5:"Rang;";i:10219;s:5:"Rarr;";i:8608;s:7:"Rarrtl;";i:10518;s:7:"Rcaron;";i:344;s:7:"Rcedil;";i:342;s:4:"Rcy;";i:1056;s:3:"Re;";i:8476;s:15:"ReverseElement;";i:8715;s:19:"ReverseEquilibrium;";i:8651;s:21:"ReverseUpEquilibrium;";i:10607;s:4:"Rfr;";i:8476;s:4:"Rho;";i:929;s:18:"RightAngleBracket;";i:10217;s:11:"RightArrow;";i:8594;s:14:"RightArrowBar;";i:8677;s:20:"RightArrowLeftArrow;";i:8644;s:13:"RightCeiling;";i:8969;s:19:"RightDoubleBracket;";i:10215;s:19:"RightDownTeeVector;";i:10589;s:16:"RightDownVector;";i:8642;s:19:"RightDownVectorBar;";i:10581;s:11:"RightFloor;";i:8971;s:9:"RightTee;";i:8866;s:14:"RightTeeArrow;";i:8614;s:15:"RightTeeVector;";i:10587;s:14:"RightTriangle;";i:8883;s:17:"RightTriangleBar;";i:10704;s:19:"RightTriangleEqual;";i:8885;s:18:"RightUpDownVector;";i:10575;s:17:"RightUpTeeVector;";i:10588;s:14:"RightUpVector;";i:8638;s:17:"RightUpVectorBar;";i:10580;s:12:"RightVector;";i:8640;s:15:"RightVectorBar;";i:10579;s:11:"Rightarrow;";i:8658;s:5:"Ropf;";i:8477;s:13:"RoundImplies;";i:10608;s:12:"Rrightarrow;";i:8667;s:5:"Rscr;";i:8475;s:4:"Rsh;";i:8625;s:12:"RuleDelayed;";i:10740;s:7:"SHCHcy;";i:1065;s:5:"SHcy;";i:1064;s:7:"SOFTcy;";i:1068;s:7:"Sacute;";i:346;s:3:"Sc;";i:10940;s:7:"Scaron;";i:352;s:7:"Scedil;";i:350;s:6:"Scirc;";i:348;s:4:"Scy;";i:1057;s:4:"Sfr;";i:120086;s:15:"ShortDownArrow;";i:8595;s:15:"ShortLeftArrow;";i:8592;s:16:"ShortRightArrow;";i:8594;s:13:"ShortUpArrow;";i:8593;s:6:"Sigma;";i:931;s:12:"SmallCircle;";i:8728;s:5:"Sopf;";i:120138;s:5:"Sqrt;";i:8730;s:7:"Square;";i:9633;s:19:"SquareIntersection;";i:8851;s:13:"SquareSubset;";i:8847;s:18:"SquareSubsetEqual;";i:8849;s:15:"SquareSuperset;";i:8848;s:20:"SquareSupersetEqual;";i:8850;s:12:"SquareUnion;";i:8852;s:5:"Sscr;";i:119982;s:5:"Star;";i:8902;s:4:"Sub;";i:8912;s:7:"Subset;";i:8912;s:12:"SubsetEqual;";i:8838;s:9:"Succeeds;";i:8827;s:14:"SucceedsEqual;";i:10928;s:19:"SucceedsSlantEqual;";i:8829;s:14:"SucceedsTilde;";i:8831;s:9:"SuchThat;";i:8715;s:4:"Sum;";i:8721;s:4:"Sup;";i:8913;s:9:"Superset;";i:8835;s:14:"SupersetEqual;";i:8839;s:7:"Supset;";i:8913;s:6:"THORN;";i:222;s:5:"THORN";i:222;s:6:"TRADE;";i:8482;s:6:"TSHcy;";i:1035;s:5:"TScy;";i:1062;s:4:"Tab;";i:9;s:4:"Tau;";i:932;s:7:"Tcaron;";i:356;s:7:"Tcedil;";i:354;s:4:"Tcy;";i:1058;s:4:"Tfr;";i:120087;s:10:"Therefore;";i:8756;s:6:"Theta;";i:920;s:10:"ThinSpace;";i:8201;s:6:"Tilde;";i:8764;s:11:"TildeEqual;";i:8771;s:15:"TildeFullEqual;";i:8773;s:11:"TildeTilde;";i:8776;s:5:"Topf;";i:120139;s:10:"TripleDot;";i:8411;s:5:"Tscr;";i:119983;s:7:"Tstrok;";i:358;s:7:"Uacute;";i:218;s:6:"Uacute";i:218;s:5:"Uarr;";i:8607;s:9:"Uarrocir;";i:10569;s:6:"Ubrcy;";i:1038;s:7:"Ubreve;";i:364;s:6:"Ucirc;";i:219;s:5:"Ucirc";i:219;s:4:"Ucy;";i:1059;s:7:"Udblac;";i:368;s:4:"Ufr;";i:120088;s:7:"Ugrave;";i:217;s:6:"Ugrave";i:217;s:6:"Umacr;";i:362;s:9:"UnderBar;";i:818;s:11:"UnderBrace;";i:9183;s:13:"UnderBracket;";i:9141;s:17:"UnderParenthesis;";i:9181;s:6:"Union;";i:8899;s:10:"UnionPlus;";i:8846;s:6:"Uogon;";i:370;s:5:"Uopf;";i:120140;s:8:"UpArrow;";i:8593;s:11:"UpArrowBar;";i:10514;s:17:"UpArrowDownArrow;";i:8645;s:12:"UpDownArrow;";i:8597;s:14:"UpEquilibrium;";i:10606;s:6:"UpTee;";i:8869;s:11:"UpTeeArrow;";i:8613;s:8:"Uparrow;";i:8657;s:12:"Updownarrow;";i:8661;s:15:"UpperLeftArrow;";i:8598;s:16:"UpperRightArrow;";i:8599;s:5:"Upsi;";i:978;s:8:"Upsilon;";i:933;s:6:"Uring;";i:366;s:5:"Uscr;";i:119984;s:7:"Utilde;";i:360;s:5:"Uuml;";i:220;s:4:"Uuml";i:220;s:6:"VDash;";i:8875;s:5:"Vbar;";i:10987;s:4:"Vcy;";i:1042;s:6:"Vdash;";i:8873;s:7:"Vdashl;";i:10982;s:4:"Vee;";i:8897;s:7:"Verbar;";i:8214;s:5:"Vert;";i:8214;s:12:"VerticalBar;";i:8739;s:13:"VerticalLine;";i:124;s:18:"VerticalSeparator;";i:10072;s:14:"VerticalTilde;";i:8768;s:14:"VeryThinSpace;";i:8202;s:4:"Vfr;";i:120089;s:5:"Vopf;";i:120141;s:5:"Vscr;";i:119985;s:7:"Vvdash;";i:8874;s:6:"Wcirc;";i:372;s:6:"Wedge;";i:8896;s:4:"Wfr;";i:120090;s:5:"Wopf;";i:120142;s:5:"Wscr;";i:119986;s:4:"Xfr;";i:120091;s:3:"Xi;";i:926;s:5:"Xopf;";i:120143;s:5:"Xscr;";i:119987;s:5:"YAcy;";i:1071;s:5:"YIcy;";i:1031;s:5:"YUcy;";i:1070;s:7:"Yacute;";i:221;s:6:"Yacute";i:221;s:6:"Ycirc;";i:374;s:4:"Ycy;";i:1067;s:4:"Yfr;";i:120092;s:5:"Yopf;";i:120144;s:5:"Yscr;";i:119988;s:5:"Yuml;";i:376;s:5:"ZHcy;";i:1046;s:7:"Zacute;";i:377;s:7:"Zcaron;";i:381;s:4:"Zcy;";i:1047;s:5:"Zdot;";i:379;s:15:"ZeroWidthSpace;";i:8203;s:5:"Zeta;";i:918;s:4:"Zfr;";i:8488;s:5:"Zopf;";i:8484;s:5:"Zscr;";i:119989;s:7:"aacute;";i:225;s:6:"aacute";i:225;s:7:"abreve;";i:259;s:3:"ac;";i:8766;s:4:"acd;";i:8767;s:6:"acirc;";i:226;s:5:"acirc";i:226;s:6:"acute;";i:180;s:5:"acute";i:180;s:4:"acy;";i:1072;s:6:"aelig;";i:230;s:5:"aelig";i:230;s:3:"af;";i:8289;s:4:"afr;";i:120094;s:7:"agrave;";i:224;s:6:"agrave";i:224;s:8:"alefsym;";i:8501;s:6:"aleph;";i:8501;s:6:"alpha;";i:945;s:6:"amacr;";i:257;s:6:"amalg;";i:10815;s:4:"amp;";i:38;s:3:"amp";i:38;s:4:"and;";i:8743;s:7:"andand;";i:10837;s:5:"andd;";i:10844;s:9:"andslope;";i:10840;s:5:"andv;";i:10842;s:4:"ang;";i:8736;s:5:"ange;";i:10660;s:6:"angle;";i:8736;s:7:"angmsd;";i:8737;s:9:"angmsdaa;";i:10664;s:9:"angmsdab;";i:10665;s:9:"angmsdac;";i:10666;s:9:"angmsdad;";i:10667;s:9:"angmsdae;";i:10668;s:9:"angmsdaf;";i:10669;s:9:"angmsdag;";i:10670;s:9:"angmsdah;";i:10671;s:6:"angrt;";i:8735;s:8:"angrtvb;";i:8894;s:9:"angrtvbd;";i:10653;s:7:"angsph;";i:8738;s:6:"angst;";i:8491;s:8:"angzarr;";i:9084;s:6:"aogon;";i:261;s:5:"aopf;";i:120146;s:3:"ap;";i:8776;s:4:"apE;";i:10864;s:7:"apacir;";i:10863;s:4:"ape;";i:8778;s:5:"apid;";i:8779;s:5:"apos;";i:39;s:7:"approx;";i:8776;s:9:"approxeq;";i:8778;s:6:"aring;";i:229;s:5:"aring";i:229;s:5:"ascr;";i:119990;s:4:"ast;";i:42;s:6:"asymp;";i:8776;s:8:"asympeq;";i:8781;s:7:"atilde;";i:227;s:6:"atilde";i:227;s:5:"auml;";i:228;s:4:"auml";i:228;s:9:"awconint;";i:8755;s:6:"awint;";i:10769;s:5:"bNot;";i:10989;s:9:"backcong;";i:8780;s:12:"backepsilon;";i:1014;s:10:"backprime;";i:8245;s:8:"backsim;";i:8765;s:10:"backsimeq;";i:8909;s:7:"barvee;";i:8893;s:7:"barwed;";i:8965;s:9:"barwedge;";i:8965;s:5:"bbrk;";i:9141;s:9:"bbrktbrk;";i:9142;s:6:"bcong;";i:8780;s:4:"bcy;";i:1073;s:6:"bdquo;";i:8222;s:7:"becaus;";i:8757;s:8:"because;";i:8757;s:8:"bemptyv;";i:10672;s:6:"bepsi;";i:1014;s:7:"bernou;";i:8492;s:5:"beta;";i:946;s:5:"beth;";i:8502;s:8:"between;";i:8812;s:4:"bfr;";i:120095;s:7:"bigcap;";i:8898;s:8:"bigcirc;";i:9711;s:7:"bigcup;";i:8899;s:8:"bigodot;";i:10752;s:9:"bigoplus;";i:10753;s:10:"bigotimes;";i:10754;s:9:"bigsqcup;";i:10758;s:8:"bigstar;";i:9733;s:16:"bigtriangledown;";i:9661;s:14:"bigtriangleup;";i:9651;s:9:"biguplus;";i:10756;s:7:"bigvee;";i:8897;s:9:"bigwedge;";i:8896;s:7:"bkarow;";i:10509;s:13:"blacklozenge;";i:10731;s:12:"blacksquare;";i:9642;s:14:"blacktriangle;";i:9652;s:18:"blacktriangledown;";i:9662;s:18:"blacktriangleleft;";i:9666;s:19:"blacktriangleright;";i:9656;s:6:"blank;";i:9251;s:6:"blk12;";i:9618;s:6:"blk14;";i:9617;s:6:"blk34;";i:9619;s:6:"block;";i:9608;s:5:"bnot;";i:8976;s:5:"bopf;";i:120147;s:4:"bot;";i:8869;s:7:"bottom;";i:8869;s:7:"bowtie;";i:8904;s:6:"boxDL;";i:9559;s:6:"boxDR;";i:9556;s:6:"boxDl;";i:9558;s:6:"boxDr;";i:9555;s:5:"boxH;";i:9552;s:6:"boxHD;";i:9574;s:6:"boxHU;";i:9577;s:6:"boxHd;";i:9572;s:6:"boxHu;";i:9575;s:6:"boxUL;";i:9565;s:6:"boxUR;";i:9562;s:6:"boxUl;";i:9564;s:6:"boxUr;";i:9561;s:5:"boxV;";i:9553;s:6:"boxVH;";i:9580;s:6:"boxVL;";i:9571;s:6:"boxVR;";i:9568;s:6:"boxVh;";i:9579;s:6:"boxVl;";i:9570;s:6:"boxVr;";i:9567;s:7:"boxbox;";i:10697;s:6:"boxdL;";i:9557;s:6:"boxdR;";i:9554;s:6:"boxdl;";i:9488;s:6:"boxdr;";i:9484;s:5:"boxh;";i:9472;s:6:"boxhD;";i:9573;s:6:"boxhU;";i:9576;s:6:"boxhd;";i:9516;s:6:"boxhu;";i:9524;s:9:"boxminus;";i:8863;s:8:"boxplus;";i:8862;s:9:"boxtimes;";i:8864;s:6:"boxuL;";i:9563;s:6:"boxuR;";i:9560;s:6:"boxul;";i:9496;s:6:"boxur;";i:9492;s:5:"boxv;";i:9474;s:6:"boxvH;";i:9578;s:6:"boxvL;";i:9569;s:6:"boxvR;";i:9566;s:6:"boxvh;";i:9532;s:6:"boxvl;";i:9508;s:6:"boxvr;";i:9500;s:7:"bprime;";i:8245;s:6:"breve;";i:728;s:7:"brvbar;";i:166;s:6:"brvbar";i:166;s:5:"bscr;";i:119991;s:6:"bsemi;";i:8271;s:5:"bsim;";i:8765;s:6:"bsime;";i:8909;s:5:"bsol;";i:92;s:6:"bsolb;";i:10693;s:5:"bull;";i:8226;s:7:"bullet;";i:8226;s:5:"bump;";i:8782;s:6:"bumpE;";i:10926;s:6:"bumpe;";i:8783;s:7:"bumpeq;";i:8783;s:7:"cacute;";i:263;s:4:"cap;";i:8745;s:7:"capand;";i:10820;s:9:"capbrcup;";i:10825;s:7:"capcap;";i:10827;s:7:"capcup;";i:10823;s:7:"capdot;";i:10816;s:6:"caret;";i:8257;s:6:"caron;";i:711;s:6:"ccaps;";i:10829;s:7:"ccaron;";i:269;s:7:"ccedil;";i:231;s:6:"ccedil";i:231;s:6:"ccirc;";i:265;s:6:"ccups;";i:10828;s:8:"ccupssm;";i:10832;s:5:"cdot;";i:267;s:6:"cedil;";i:184;s:5:"cedil";i:184;s:8:"cemptyv;";i:10674;s:5:"cent;";i:162;s:4:"cent";i:162;s:10:"centerdot;";i:183;s:4:"cfr;";i:120096;s:5:"chcy;";i:1095;s:6:"check;";i:10003;s:10:"checkmark;";i:10003;s:4:"chi;";i:967;s:4:"cir;";i:9675;s:5:"cirE;";i:10691;s:5:"circ;";i:710;s:7:"circeq;";i:8791;s:16:"circlearrowleft;";i:8634;s:17:"circlearrowright;";i:8635;s:9:"circledR;";i:174;s:9:"circledS;";i:9416;s:11:"circledast;";i:8859;s:12:"circledcirc;";i:8858;s:12:"circleddash;";i:8861;s:5:"cire;";i:8791;s:9:"cirfnint;";i:10768;s:7:"cirmid;";i:10991;s:8:"cirscir;";i:10690;s:6:"clubs;";i:9827;s:9:"clubsuit;";i:9827;s:6:"colon;";i:58;s:7:"colone;";i:8788;s:8:"coloneq;";i:8788;s:6:"comma;";i:44;s:7:"commat;";i:64;s:5:"comp;";i:8705;s:7:"compfn;";i:8728;s:11:"complement;";i:8705;s:10:"complexes;";i:8450;s:5:"cong;";i:8773;s:8:"congdot;";i:10861;s:7:"conint;";i:8750;s:5:"copf;";i:120148;s:7:"coprod;";i:8720;s:5:"copy;";i:169;s:4:"copy";i:169;s:7:"copysr;";i:8471;s:6:"crarr;";i:8629;s:6:"cross;";i:10007;s:5:"cscr;";i:119992;s:5:"csub;";i:10959;s:6:"csube;";i:10961;s:5:"csup;";i:10960;s:6:"csupe;";i:10962;s:6:"ctdot;";i:8943;s:8:"cudarrl;";i:10552;s:8:"cudarrr;";i:10549;s:6:"cuepr;";i:8926;s:6:"cuesc;";i:8927;s:7:"cularr;";i:8630;s:8:"cularrp;";i:10557;s:4:"cup;";i:8746;s:9:"cupbrcap;";i:10824;s:7:"cupcap;";i:10822;s:7:"cupcup;";i:10826;s:7:"cupdot;";i:8845;s:6:"cupor;";i:10821;s:7:"curarr;";i:8631;s:8:"curarrm;";i:10556;s:12:"curlyeqprec;";i:8926;s:12:"curlyeqsucc;";i:8927;s:9:"curlyvee;";i:8910;s:11:"curlywedge;";i:8911;s:7:"curren;";i:164;s:6:"curren";i:164;s:15:"curvearrowleft;";i:8630;s:16:"curvearrowright;";i:8631;s:6:"cuvee;";i:8910;s:6:"cuwed;";i:8911;s:9:"cwconint;";i:8754;s:6:"cwint;";i:8753;s:7:"cylcty;";i:9005;s:5:"dArr;";i:8659;s:5:"dHar;";i:10597;s:7:"dagger;";i:8224;s:7:"daleth;";i:8504;s:5:"darr;";i:8595;s:5:"dash;";i:8208;s:6:"dashv;";i:8867;s:8:"dbkarow;";i:10511;s:6:"dblac;";i:733;s:7:"dcaron;";i:271;s:4:"dcy;";i:1076;s:3:"dd;";i:8518;s:8:"ddagger;";i:8225;s:6:"ddarr;";i:8650;s:8:"ddotseq;";i:10871;s:4:"deg;";i:176;s:3:"deg";i:176;s:6:"delta;";i:948;s:8:"demptyv;";i:10673;s:7:"dfisht;";i:10623;s:4:"dfr;";i:120097;s:6:"dharl;";i:8643;s:6:"dharr;";i:8642;s:5:"diam;";i:8900;s:8:"diamond;";i:8900;s:12:"diamondsuit;";i:9830;s:6:"diams;";i:9830;s:4:"die;";i:168;s:8:"digamma;";i:989;s:6:"disin;";i:8946;s:4:"div;";i:247;s:7:"divide;";i:247;s:6:"divide";i:247;s:14:"divideontimes;";i:8903;s:7:"divonx;";i:8903;s:5:"djcy;";i:1106;s:7:"dlcorn;";i:8990;s:7:"dlcrop;";i:8973;s:7:"dollar;";i:36;s:5:"dopf;";i:120149;s:4:"dot;";i:729;s:6:"doteq;";i:8784;s:9:"doteqdot;";i:8785;s:9:"dotminus;";i:8760;s:8:"dotplus;";i:8724;s:10:"dotsquare;";i:8865;s:15:"doublebarwedge;";i:8966;s:10:"downarrow;";i:8595;s:15:"downdownarrows;";i:8650;s:16:"downharpoonleft;";i:8643;s:17:"downharpoonright;";i:8642;s:9:"drbkarow;";i:10512;s:7:"drcorn;";i:8991;s:7:"drcrop;";i:8972;s:5:"dscr;";i:119993;s:5:"dscy;";i:1109;s:5:"dsol;";i:10742;s:7:"dstrok;";i:273;s:6:"dtdot;";i:8945;s:5:"dtri;";i:9663;s:6:"dtrif;";i:9662;s:6:"duarr;";i:8693;s:6:"duhar;";i:10607;s:8:"dwangle;";i:10662;s:5:"dzcy;";i:1119;s:9:"dzigrarr;";i:10239;s:6:"eDDot;";i:10871;s:5:"eDot;";i:8785;s:7:"eacute;";i:233;s:6:"eacute";i:233;s:7:"easter;";i:10862;s:7:"ecaron;";i:283;s:5:"ecir;";i:8790;s:6:"ecirc;";i:234;s:5:"ecirc";i:234;s:7:"ecolon;";i:8789;s:4:"ecy;";i:1101;s:5:"edot;";i:279;s:3:"ee;";i:8519;s:6:"efDot;";i:8786;s:4:"efr;";i:120098;s:3:"eg;";i:10906;s:7:"egrave;";i:232;s:6:"egrave";i:232;s:4:"egs;";i:10902;s:7:"egsdot;";i:10904;s:3:"el;";i:10905;s:9:"elinters;";i:9191;s:4:"ell;";i:8467;s:4:"els;";i:10901;s:7:"elsdot;";i:10903;s:6:"emacr;";i:275;s:6:"empty;";i:8709;s:9:"emptyset;";i:8709;s:7:"emptyv;";i:8709;s:7:"emsp13;";i:8196;s:7:"emsp14;";i:8197;s:5:"emsp;";i:8195;s:4:"eng;";i:331;s:5:"ensp;";i:8194;s:6:"eogon;";i:281;s:5:"eopf;";i:120150;s:5:"epar;";i:8917;s:7:"eparsl;";i:10723;s:6:"eplus;";i:10865;s:5:"epsi;";i:1013;s:8:"epsilon;";i:949;s:6:"epsiv;";i:949;s:7:"eqcirc;";i:8790;s:8:"eqcolon;";i:8789;s:6:"eqsim;";i:8770;s:11:"eqslantgtr;";i:10902;s:12:"eqslantless;";i:10901;s:7:"equals;";i:61;s:7:"equest;";i:8799;s:6:"equiv;";i:8801;s:8:"equivDD;";i:10872;s:9:"eqvparsl;";i:10725;s:6:"erDot;";i:8787;s:6:"erarr;";i:10609;s:5:"escr;";i:8495;s:6:"esdot;";i:8784;s:5:"esim;";i:8770;s:4:"eta;";i:951;s:4:"eth;";i:240;s:3:"eth";i:240;s:5:"euml;";i:235;s:4:"euml";i:235;s:5:"euro;";i:8364;s:5:"excl;";i:33;s:6:"exist;";i:8707;s:12:"expectation;";i:8496;s:13:"exponentiale;";i:8519;s:14:"fallingdotseq;";i:8786;s:4:"fcy;";i:1092;s:7:"female;";i:9792;s:7:"ffilig;";i:64259;s:6:"fflig;";i:64256;s:7:"ffllig;";i:64260;s:4:"ffr;";i:120099;s:6:"filig;";i:64257;s:5:"flat;";i:9837;s:6:"fllig;";i:64258;s:6:"fltns;";i:9649;s:5:"fnof;";i:402;s:5:"fopf;";i:120151;s:7:"forall;";i:8704;s:5:"fork;";i:8916;s:6:"forkv;";i:10969;s:9:"fpartint;";i:10765;s:7:"frac12;";i:189;s:6:"frac12";i:189;s:7:"frac13;";i:8531;s:7:"frac14;";i:188;s:6:"frac14";i:188;s:7:"frac15;";i:8533;s:7:"frac16;";i:8537;s:7:"frac18;";i:8539;s:7:"frac23;";i:8532;s:7:"frac25;";i:8534;s:7:"frac34;";i:190;s:6:"frac34";i:190;s:7:"frac35;";i:8535;s:7:"frac38;";i:8540;s:7:"frac45;";i:8536;s:7:"frac56;";i:8538;s:7:"frac58;";i:8541;s:7:"frac78;";i:8542;s:6:"frasl;";i:8260;s:6:"frown;";i:8994;s:5:"fscr;";i:119995;s:3:"gE;";i:8807;s:4:"gEl;";i:10892;s:7:"gacute;";i:501;s:6:"gamma;";i:947;s:7:"gammad;";i:989;s:4:"gap;";i:10886;s:7:"gbreve;";i:287;s:6:"gcirc;";i:285;s:4:"gcy;";i:1075;s:5:"gdot;";i:289;s:3:"ge;";i:8805;s:4:"gel;";i:8923;s:4:"geq;";i:8805;s:5:"geqq;";i:8807;s:9:"geqslant;";i:10878;s:4:"ges;";i:10878;s:6:"gescc;";i:10921;s:7:"gesdot;";i:10880;s:8:"gesdoto;";i:10882;s:9:"gesdotol;";i:10884;s:7:"gesles;";i:10900;s:4:"gfr;";i:120100;s:3:"gg;";i:8811;s:4:"ggg;";i:8921;s:6:"gimel;";i:8503;s:5:"gjcy;";i:1107;s:3:"gl;";i:8823;s:4:"glE;";i:10898;s:4:"gla;";i:10917;s:4:"glj;";i:10916;s:4:"gnE;";i:8809;s:5:"gnap;";i:10890;s:9:"gnapprox;";i:10890;s:4:"gne;";i:10888;s:5:"gneq;";i:10888;s:6:"gneqq;";i:8809;s:6:"gnsim;";i:8935;s:5:"gopf;";i:120152;s:6:"grave;";i:96;s:5:"gscr;";i:8458;s:5:"gsim;";i:8819;s:6:"gsime;";i:10894;s:6:"gsiml;";i:10896;s:3:"gt;";i:62;s:2:"gt";i:62;s:5:"gtcc;";i:10919;s:6:"gtcir;";i:10874;s:6:"gtdot;";i:8919;s:7:"gtlPar;";i:10645;s:8:"gtquest;";i:10876;s:10:"gtrapprox;";i:10886;s:7:"gtrarr;";i:10616;s:7:"gtrdot;";i:8919;s:10:"gtreqless;";i:8923;s:11:"gtreqqless;";i:10892;s:8:"gtrless;";i:8823;s:7:"gtrsim;";i:8819;s:5:"hArr;";i:8660;s:7:"hairsp;";i:8202;s:5:"half;";i:189;s:7:"hamilt;";i:8459;s:7:"hardcy;";i:1098;s:5:"harr;";i:8596;s:8:"harrcir;";i:10568;s:6:"harrw;";i:8621;s:5:"hbar;";i:8463;s:6:"hcirc;";i:293;s:7:"hearts;";i:9829;s:10:"heartsuit;";i:9829;s:7:"hellip;";i:8230;s:7:"hercon;";i:8889;s:4:"hfr;";i:120101;s:9:"hksearow;";i:10533;s:9:"hkswarow;";i:10534;s:6:"hoarr;";i:8703;s:7:"homtht;";i:8763;s:14:"hookleftarrow;";i:8617;s:15:"hookrightarrow;";i:8618;s:5:"hopf;";i:120153;s:7:"horbar;";i:8213;s:5:"hscr;";i:119997;s:7:"hslash;";i:8463;s:7:"hstrok;";i:295;s:7:"hybull;";i:8259;s:7:"hyphen;";i:8208;s:7:"iacute;";i:237;s:6:"iacute";i:237;s:3:"ic;";i:8291;s:6:"icirc;";i:238;s:5:"icirc";i:238;s:4:"icy;";i:1080;s:5:"iecy;";i:1077;s:6:"iexcl;";i:161;s:5:"iexcl";i:161;s:4:"iff;";i:8660;s:4:"ifr;";i:120102;s:7:"igrave;";i:236;s:6:"igrave";i:236;s:3:"ii;";i:8520;s:7:"iiiint;";i:10764;s:6:"iiint;";i:8749;s:7:"iinfin;";i:10716;s:6:"iiota;";i:8489;s:6:"ijlig;";i:307;s:6:"imacr;";i:299;s:6:"image;";i:8465;s:9:"imagline;";i:8464;s:9:"imagpart;";i:8465;s:6:"imath;";i:305;s:5:"imof;";i:8887;s:6:"imped;";i:437;s:3:"in;";i:8712;s:7:"incare;";i:8453;s:6:"infin;";i:8734;s:9:"infintie;";i:10717;s:7:"inodot;";i:305;s:4:"int;";i:8747;s:7:"intcal;";i:8890;s:9:"integers;";i:8484;s:9:"intercal;";i:8890;s:9:"intlarhk;";i:10775;s:8:"intprod;";i:10812;s:5:"iocy;";i:1105;s:6:"iogon;";i:303;s:5:"iopf;";i:120154;s:5:"iota;";i:953;s:6:"iprod;";i:10812;s:7:"iquest;";i:191;s:6:"iquest";i:191;s:5:"iscr;";i:119998;s:5:"isin;";i:8712;s:6:"isinE;";i:8953;s:8:"isindot;";i:8949;s:6:"isins;";i:8948;s:7:"isinsv;";i:8947;s:6:"isinv;";i:8712;s:3:"it;";i:8290;s:7:"itilde;";i:297;s:6:"iukcy;";i:1110;s:5:"iuml;";i:239;s:4:"iuml";i:239;s:6:"jcirc;";i:309;s:4:"jcy;";i:1081;s:4:"jfr;";i:120103;s:6:"jmath;";i:567;s:5:"jopf;";i:120155;s:5:"jscr;";i:119999;s:7:"jsercy;";i:1112;s:6:"jukcy;";i:1108;s:6:"kappa;";i:954;s:7:"kappav;";i:1008;s:7:"kcedil;";i:311;s:4:"kcy;";i:1082;s:4:"kfr;";i:120104;s:7:"kgreen;";i:312;s:5:"khcy;";i:1093;s:5:"kjcy;";i:1116;s:5:"kopf;";i:120156;s:5:"kscr;";i:120000;s:6:"lAarr;";i:8666;s:5:"lArr;";i:8656;s:7:"lAtail;";i:10523;s:6:"lBarr;";i:10510;s:3:"lE;";i:8806;s:4:"lEg;";i:10891;s:5:"lHar;";i:10594;s:7:"lacute;";i:314;s:9:"laemptyv;";i:10676;s:7:"lagran;";i:8466;s:7:"lambda;";i:955;s:5:"lang;";i:10216;s:6:"langd;";i:10641;s:7:"langle;";i:10216;s:4:"lap;";i:10885;s:6:"laquo;";i:171;s:5:"laquo";i:171;s:5:"larr;";i:8592;s:6:"larrb;";i:8676;s:8:"larrbfs;";i:10527;s:7:"larrfs;";i:10525;s:7:"larrhk;";i:8617;s:7:"larrlp;";i:8619;s:7:"larrpl;";i:10553;s:8:"larrsim;";i:10611;s:7:"larrtl;";i:8610;s:4:"lat;";i:10923;s:7:"latail;";i:10521;s:5:"late;";i:10925;s:6:"lbarr;";i:10508;s:6:"lbbrk;";i:10098;s:7:"lbrace;";i:123;s:7:"lbrack;";i:91;s:6:"lbrke;";i:10635;s:8:"lbrksld;";i:10639;s:8:"lbrkslu;";i:10637;s:7:"lcaron;";i:318;s:7:"lcedil;";i:316;s:6:"lceil;";i:8968;s:5:"lcub;";i:123;s:4:"lcy;";i:1083;s:5:"ldca;";i:10550;s:6:"ldquo;";i:8220;s:7:"ldquor;";i:8222;s:8:"ldrdhar;";i:10599;s:9:"ldrushar;";i:10571;s:5:"ldsh;";i:8626;s:3:"le;";i:8804;s:10:"leftarrow;";i:8592;s:14:"leftarrowtail;";i:8610;s:16:"leftharpoondown;";i:8637;s:14:"leftharpoonup;";i:8636;s:15:"leftleftarrows;";i:8647;s:15:"leftrightarrow;";i:8596;s:16:"leftrightarrows;";i:8646;s:18:"leftrightharpoons;";i:8651;s:20:"leftrightsquigarrow;";i:8621;s:15:"leftthreetimes;";i:8907;s:4:"leg;";i:8922;s:4:"leq;";i:8804;s:5:"leqq;";i:8806;s:9:"leqslant;";i:10877;s:4:"les;";i:10877;s:6:"lescc;";i:10920;s:7:"lesdot;";i:10879;s:8:"lesdoto;";i:10881;s:9:"lesdotor;";i:10883;s:7:"lesges;";i:10899;s:11:"lessapprox;";i:10885;s:8:"lessdot;";i:8918;s:10:"lesseqgtr;";i:8922;s:11:"lesseqqgtr;";i:10891;s:8:"lessgtr;";i:8822;s:8:"lesssim;";i:8818;s:7:"lfisht;";i:10620;s:7:"lfloor;";i:8970;s:4:"lfr;";i:120105;s:3:"lg;";i:8822;s:4:"lgE;";i:10897;s:6:"lhard;";i:8637;s:6:"lharu;";i:8636;s:7:"lharul;";i:10602;s:6:"lhblk;";i:9604;s:5:"ljcy;";i:1113;s:3:"ll;";i:8810;s:6:"llarr;";i:8647;s:9:"llcorner;";i:8990;s:7:"llhard;";i:10603;s:6:"lltri;";i:9722;s:7:"lmidot;";i:320;s:7:"lmoust;";i:9136;s:11:"lmoustache;";i:9136;s:4:"lnE;";i:8808;s:5:"lnap;";i:10889;s:9:"lnapprox;";i:10889;s:4:"lne;";i:10887;s:5:"lneq;";i:10887;s:6:"lneqq;";i:8808;s:6:"lnsim;";i:8934;s:6:"loang;";i:10220;s:6:"loarr;";i:8701;s:6:"lobrk;";i:10214;s:14:"longleftarrow;";i:10229;s:19:"longleftrightarrow;";i:10231;s:11:"longmapsto;";i:10236;s:15:"longrightarrow;";i:10230;s:14:"looparrowleft;";i:8619;s:15:"looparrowright;";i:8620;s:6:"lopar;";i:10629;s:5:"lopf;";i:120157;s:7:"loplus;";i:10797;s:8:"lotimes;";i:10804;s:7:"lowast;";i:8727;s:7:"lowbar;";i:95;s:4:"loz;";i:9674;s:8:"lozenge;";i:9674;s:5:"lozf;";i:10731;s:5:"lpar;";i:40;s:7:"lparlt;";i:10643;s:6:"lrarr;";i:8646;s:9:"lrcorner;";i:8991;s:6:"lrhar;";i:8651;s:7:"lrhard;";i:10605;s:4:"lrm;";i:8206;s:6:"lrtri;";i:8895;s:7:"lsaquo;";i:8249;s:5:"lscr;";i:120001;s:4:"lsh;";i:8624;s:5:"lsim;";i:8818;s:6:"lsime;";i:10893;s:6:"lsimg;";i:10895;s:5:"lsqb;";i:91;s:6:"lsquo;";i:8216;s:7:"lsquor;";i:8218;s:7:"lstrok;";i:322;s:3:"lt;";i:60;s:2:"lt";i:60;s:5:"ltcc;";i:10918;s:6:"ltcir;";i:10873;s:6:"ltdot;";i:8918;s:7:"lthree;";i:8907;s:7:"ltimes;";i:8905;s:7:"ltlarr;";i:10614;s:8:"ltquest;";i:10875;s:7:"ltrPar;";i:10646;s:5:"ltri;";i:9667;s:6:"ltrie;";i:8884;s:6:"ltrif;";i:9666;s:9:"lurdshar;";i:10570;s:8:"luruhar;";i:10598;s:6:"mDDot;";i:8762;s:5:"macr;";i:175;s:4:"macr";i:175;s:5:"male;";i:9794;s:5:"malt;";i:10016;s:8:"maltese;";i:10016;s:4:"map;";i:8614;s:7:"mapsto;";i:8614;s:11:"mapstodown;";i:8615;s:11:"mapstoleft;";i:8612;s:9:"mapstoup;";i:8613;s:7:"marker;";i:9646;s:7:"mcomma;";i:10793;s:4:"mcy;";i:1084;s:6:"mdash;";i:8212;s:14:"measuredangle;";i:8737;s:4:"mfr;";i:120106;s:4:"mho;";i:8487;s:6:"micro;";i:181;s:5:"micro";i:181;s:4:"mid;";i:8739;s:7:"midast;";i:42;s:7:"midcir;";i:10992;s:7:"middot;";i:183;s:6:"middot";i:183;s:6:"minus;";i:8722;s:7:"minusb;";i:8863;s:7:"minusd;";i:8760;s:8:"minusdu;";i:10794;s:5:"mlcp;";i:10971;s:5:"mldr;";i:8230;s:7:"mnplus;";i:8723;s:7:"models;";i:8871;s:5:"mopf;";i:120158;s:3:"mp;";i:8723;s:5:"mscr;";i:120002;s:7:"mstpos;";i:8766;s:3:"mu;";i:956;s:9:"multimap;";i:8888;s:6:"mumap;";i:8888;s:11:"nLeftarrow;";i:8653;s:16:"nLeftrightarrow;";i:8654;s:12:"nRightarrow;";i:8655;s:7:"nVDash;";i:8879;s:7:"nVdash;";i:8878;s:6:"nabla;";i:8711;s:7:"nacute;";i:324;s:4:"nap;";i:8777;s:6:"napos;";i:329;s:8:"napprox;";i:8777;s:6:"natur;";i:9838;s:8:"natural;";i:9838;s:9:"naturals;";i:8469;s:5:"nbsp;";i:160;s:4:"nbsp";i:160;s:5:"ncap;";i:10819;s:7:"ncaron;";i:328;s:7:"ncedil;";i:326;s:6:"ncong;";i:8775;s:5:"ncup;";i:10818;s:4:"ncy;";i:1085;s:6:"ndash;";i:8211;s:3:"ne;";i:8800;s:6:"neArr;";i:8663;s:7:"nearhk;";i:10532;s:6:"nearr;";i:8599;s:8:"nearrow;";i:8599;s:7:"nequiv;";i:8802;s:7:"nesear;";i:10536;s:7:"nexist;";i:8708;s:8:"nexists;";i:8708;s:4:"nfr;";i:120107;s:4:"nge;";i:8817;s:5:"ngeq;";i:8817;s:6:"ngsim;";i:8821;s:4:"ngt;";i:8815;s:5:"ngtr;";i:8815;s:6:"nhArr;";i:8654;s:6:"nharr;";i:8622;s:6:"nhpar;";i:10994;s:3:"ni;";i:8715;s:4:"nis;";i:8956;s:5:"nisd;";i:8954;s:4:"niv;";i:8715;s:5:"njcy;";i:1114;s:6:"nlArr;";i:8653;s:6:"nlarr;";i:8602;s:5:"nldr;";i:8229;s:4:"nle;";i:8816;s:11:"nleftarrow;";i:8602;s:16:"nleftrightarrow;";i:8622;s:5:"nleq;";i:8816;s:6:"nless;";i:8814;s:6:"nlsim;";i:8820;s:4:"nlt;";i:8814;s:6:"nltri;";i:8938;s:7:"nltrie;";i:8940;s:5:"nmid;";i:8740;s:5:"nopf;";i:120159;s:4:"not;";i:172;s:3:"not";i:172;s:6:"notin;";i:8713;s:8:"notinva;";i:8713;s:8:"notinvb;";i:8951;s:8:"notinvc;";i:8950;s:6:"notni;";i:8716;s:8:"notniva;";i:8716;s:8:"notnivb;";i:8958;s:8:"notnivc;";i:8957;s:5:"npar;";i:8742;s:10:"nparallel;";i:8742;s:8:"npolint;";i:10772;s:4:"npr;";i:8832;s:7:"nprcue;";i:8928;s:6:"nprec;";i:8832;s:6:"nrArr;";i:8655;s:6:"nrarr;";i:8603;s:12:"nrightarrow;";i:8603;s:6:"nrtri;";i:8939;s:7:"nrtrie;";i:8941;s:4:"nsc;";i:8833;s:7:"nsccue;";i:8929;s:5:"nscr;";i:120003;s:10:"nshortmid;";i:8740;s:15:"nshortparallel;";i:8742;s:5:"nsim;";i:8769;s:6:"nsime;";i:8772;s:7:"nsimeq;";i:8772;s:6:"nsmid;";i:8740;s:6:"nspar;";i:8742;s:8:"nsqsube;";i:8930;s:8:"nsqsupe;";i:8931;s:5:"nsub;";i:8836;s:6:"nsube;";i:8840;s:10:"nsubseteq;";i:8840;s:6:"nsucc;";i:8833;s:5:"nsup;";i:8837;s:6:"nsupe;";i:8841;s:10:"nsupseteq;";i:8841;s:5:"ntgl;";i:8825;s:7:"ntilde;";i:241;s:6:"ntilde";i:241;s:5:"ntlg;";i:8824;s:14:"ntriangleleft;";i:8938;s:16:"ntrianglelefteq;";i:8940;s:15:"ntriangleright;";i:8939;s:17:"ntrianglerighteq;";i:8941;s:3:"nu;";i:957;s:4:"num;";i:35;s:7:"numero;";i:8470;s:6:"numsp;";i:8199;s:7:"nvDash;";i:8877;s:7:"nvHarr;";i:10500;s:7:"nvdash;";i:8876;s:8:"nvinfin;";i:10718;s:7:"nvlArr;";i:10498;s:7:"nvrArr;";i:10499;s:6:"nwArr;";i:8662;s:7:"nwarhk;";i:10531;s:6:"nwarr;";i:8598;s:8:"nwarrow;";i:8598;s:7:"nwnear;";i:10535;s:3:"oS;";i:9416;s:7:"oacute;";i:243;s:6:"oacute";i:243;s:5:"oast;";i:8859;s:5:"ocir;";i:8858;s:6:"ocirc;";i:244;s:5:"ocirc";i:244;s:4:"ocy;";i:1086;s:6:"odash;";i:8861;s:7:"odblac;";i:337;s:5:"odiv;";i:10808;s:5:"odot;";i:8857;s:7:"odsold;";i:10684;s:6:"oelig;";i:339;s:6:"ofcir;";i:10687;s:4:"ofr;";i:120108;s:5:"ogon;";i:731;s:7:"ograve;";i:242;s:6:"ograve";i:242;s:4:"ogt;";i:10689;s:6:"ohbar;";i:10677;s:4:"ohm;";i:8486;s:5:"oint;";i:8750;s:6:"olarr;";i:8634;s:6:"olcir;";i:10686;s:8:"olcross;";i:10683;s:6:"oline;";i:8254;s:4:"olt;";i:10688;s:6:"omacr;";i:333;s:6:"omega;";i:969;s:8:"omicron;";i:959;s:5:"omid;";i:10678;s:7:"ominus;";i:8854;s:5:"oopf;";i:120160;s:5:"opar;";i:10679;s:6:"operp;";i:10681;s:6:"oplus;";i:8853;s:3:"or;";i:8744;s:6:"orarr;";i:8635;s:4:"ord;";i:10845;s:6:"order;";i:8500;s:8:"orderof;";i:8500;s:5:"ordf;";i:170;s:4:"ordf";i:170;s:5:"ordm;";i:186;s:4:"ordm";i:186;s:7:"origof;";i:8886;s:5:"oror;";i:10838;s:8:"orslope;";i:10839;s:4:"orv;";i:10843;s:5:"oscr;";i:8500;s:7:"oslash;";i:248;s:6:"oslash";i:248;s:5:"osol;";i:8856;s:7:"otilde;";i:245;s:6:"otilde";i:245;s:7:"otimes;";i:8855;s:9:"otimesas;";i:10806;s:5:"ouml;";i:246;s:4:"ouml";i:246;s:6:"ovbar;";i:9021;s:4:"par;";i:8741;s:5:"para;";i:182;s:4:"para";i:182;s:9:"parallel;";i:8741;s:7:"parsim;";i:10995;s:6:"parsl;";i:11005;s:5:"part;";i:8706;s:4:"pcy;";i:1087;s:7:"percnt;";i:37;s:7:"period;";i:46;s:7:"permil;";i:8240;s:5:"perp;";i:8869;s:8:"pertenk;";i:8241;s:4:"pfr;";i:120109;s:4:"phi;";i:966;s:5:"phiv;";i:966;s:7:"phmmat;";i:8499;s:6:"phone;";i:9742;s:3:"pi;";i:960;s:10:"pitchfork;";i:8916;s:4:"piv;";i:982;s:7:"planck;";i:8463;s:8:"planckh;";i:8462;s:7:"plankv;";i:8463;s:5:"plus;";i:43;s:9:"plusacir;";i:10787;s:6:"plusb;";i:8862;s:8:"pluscir;";i:10786;s:7:"plusdo;";i:8724;s:7:"plusdu;";i:10789;s:6:"pluse;";i:10866;s:7:"plusmn;";i:177;s:6:"plusmn";i:177;s:8:"plussim;";i:10790;s:8:"plustwo;";i:10791;s:3:"pm;";i:177;s:9:"pointint;";i:10773;s:5:"popf;";i:120161;s:6:"pound;";i:163;s:5:"pound";i:163;s:3:"pr;";i:8826;s:4:"prE;";i:10931;s:5:"prap;";i:10935;s:6:"prcue;";i:8828;s:4:"pre;";i:10927;s:5:"prec;";i:8826;s:11:"precapprox;";i:10935;s:12:"preccurlyeq;";i:8828;s:7:"preceq;";i:10927;s:12:"precnapprox;";i:10937;s:9:"precneqq;";i:10933;s:9:"precnsim;";i:8936;s:8:"precsim;";i:8830;s:6:"prime;";i:8242;s:7:"primes;";i:8473;s:5:"prnE;";i:10933;s:6:"prnap;";i:10937;s:7:"prnsim;";i:8936;s:5:"prod;";i:8719;s:9:"profalar;";i:9006;s:9:"profline;";i:8978;s:9:"profsurf;";i:8979;s:5:"prop;";i:8733;s:7:"propto;";i:8733;s:6:"prsim;";i:8830;s:7:"prurel;";i:8880;s:5:"pscr;";i:120005;s:4:"psi;";i:968;s:7:"puncsp;";i:8200;s:4:"qfr;";i:120110;s:5:"qint;";i:10764;s:5:"qopf;";i:120162;s:7:"qprime;";i:8279;s:5:"qscr;";i:120006;s:12:"quaternions;";i:8461;s:8:"quatint;";i:10774;s:6:"quest;";i:63;s:8:"questeq;";i:8799;s:5:"quot;";i:34;s:4:"quot";i:34;s:6:"rAarr;";i:8667;s:5:"rArr;";i:8658;s:7:"rAtail;";i:10524;s:6:"rBarr;";i:10511;s:5:"rHar;";i:10596;s:5:"race;";i:10714;s:7:"racute;";i:341;s:6:"radic;";i:8730;s:9:"raemptyv;";i:10675;s:5:"rang;";i:10217;s:6:"rangd;";i:10642;s:6:"range;";i:10661;s:7:"rangle;";i:10217;s:6:"raquo;";i:187;s:5:"raquo";i:187;s:5:"rarr;";i:8594;s:7:"rarrap;";i:10613;s:6:"rarrb;";i:8677;s:8:"rarrbfs;";i:10528;s:6:"rarrc;";i:10547;s:7:"rarrfs;";i:10526;s:7:"rarrhk;";i:8618;s:7:"rarrlp;";i:8620;s:7:"rarrpl;";i:10565;s:8:"rarrsim;";i:10612;s:7:"rarrtl;";i:8611;s:6:"rarrw;";i:8605;s:7:"ratail;";i:10522;s:6:"ratio;";i:8758;s:10:"rationals;";i:8474;s:6:"rbarr;";i:10509;s:6:"rbbrk;";i:10099;s:7:"rbrace;";i:125;s:7:"rbrack;";i:93;s:6:"rbrke;";i:10636;s:8:"rbrksld;";i:10638;s:8:"rbrkslu;";i:10640;s:7:"rcaron;";i:345;s:7:"rcedil;";i:343;s:6:"rceil;";i:8969;s:5:"rcub;";i:125;s:4:"rcy;";i:1088;s:5:"rdca;";i:10551;s:8:"rdldhar;";i:10601;s:6:"rdquo;";i:8221;s:7:"rdquor;";i:8221;s:5:"rdsh;";i:8627;s:5:"real;";i:8476;s:8:"realine;";i:8475;s:9:"realpart;";i:8476;s:6:"reals;";i:8477;s:5:"rect;";i:9645;s:4:"reg;";i:174;s:3:"reg";i:174;s:7:"rfisht;";i:10621;s:7:"rfloor;";i:8971;s:4:"rfr;";i:120111;s:6:"rhard;";i:8641;s:6:"rharu;";i:8640;s:7:"rharul;";i:10604;s:4:"rho;";i:961;s:5:"rhov;";i:1009;s:11:"rightarrow;";i:8594;s:15:"rightarrowtail;";i:8611;s:17:"rightharpoondown;";i:8641;s:15:"rightharpoonup;";i:8640;s:16:"rightleftarrows;";i:8644;s:18:"rightleftharpoons;";i:8652;s:17:"rightrightarrows;";i:8649;s:16:"rightsquigarrow;";i:8605;s:16:"rightthreetimes;";i:8908;s:5:"ring;";i:730;s:13:"risingdotseq;";i:8787;s:6:"rlarr;";i:8644;s:6:"rlhar;";i:8652;s:4:"rlm;";i:8207;s:7:"rmoust;";i:9137;s:11:"rmoustache;";i:9137;s:6:"rnmid;";i:10990;s:6:"roang;";i:10221;s:6:"roarr;";i:8702;s:6:"robrk;";i:10215;s:6:"ropar;";i:10630;s:5:"ropf;";i:120163;s:7:"roplus;";i:10798;s:8:"rotimes;";i:10805;s:5:"rpar;";i:41;s:7:"rpargt;";i:10644;s:9:"rppolint;";i:10770;s:6:"rrarr;";i:8649;s:7:"rsaquo;";i:8250;s:5:"rscr;";i:120007;s:4:"rsh;";i:8625;s:5:"rsqb;";i:93;s:6:"rsquo;";i:8217;s:7:"rsquor;";i:8217;s:7:"rthree;";i:8908;s:7:"rtimes;";i:8906;s:5:"rtri;";i:9657;s:6:"rtrie;";i:8885;s:6:"rtrif;";i:9656;s:9:"rtriltri;";i:10702;s:8:"ruluhar;";i:10600;s:3:"rx;";i:8478;s:7:"sacute;";i:347;s:6:"sbquo;";i:8218;s:3:"sc;";i:8827;s:4:"scE;";i:10932;s:5:"scap;";i:10936;s:7:"scaron;";i:353;s:6:"sccue;";i:8829;s:4:"sce;";i:10928;s:7:"scedil;";i:351;s:6:"scirc;";i:349;s:5:"scnE;";i:10934;s:6:"scnap;";i:10938;s:7:"scnsim;";i:8937;s:9:"scpolint;";i:10771;s:6:"scsim;";i:8831;s:4:"scy;";i:1089;s:5:"sdot;";i:8901;s:6:"sdotb;";i:8865;s:6:"sdote;";i:10854;s:6:"seArr;";i:8664;s:7:"searhk;";i:10533;s:6:"searr;";i:8600;s:8:"searrow;";i:8600;s:5:"sect;";i:167;s:4:"sect";i:167;s:5:"semi;";i:59;s:7:"seswar;";i:10537;s:9:"setminus;";i:8726;s:6:"setmn;";i:8726;s:5:"sext;";i:10038;s:4:"sfr;";i:120112;s:7:"sfrown;";i:8994;s:6:"sharp;";i:9839;s:7:"shchcy;";i:1097;s:5:"shcy;";i:1096;s:9:"shortmid;";i:8739;s:14:"shortparallel;";i:8741;s:4:"shy;";i:173;s:3:"shy";i:173;s:6:"sigma;";i:963;s:7:"sigmaf;";i:962;s:7:"sigmav;";i:962;s:4:"sim;";i:8764;s:7:"simdot;";i:10858;s:5:"sime;";i:8771;s:6:"simeq;";i:8771;s:5:"simg;";i:10910;s:6:"simgE;";i:10912;s:5:"siml;";i:10909;s:6:"simlE;";i:10911;s:6:"simne;";i:8774;s:8:"simplus;";i:10788;s:8:"simrarr;";i:10610;s:6:"slarr;";i:8592;s:14:"smallsetminus;";i:8726;s:7:"smashp;";i:10803;s:9:"smeparsl;";i:10724;s:5:"smid;";i:8739;s:6:"smile;";i:8995;s:4:"smt;";i:10922;s:5:"smte;";i:10924;s:7:"softcy;";i:1100;s:4:"sol;";i:47;s:5:"solb;";i:10692;s:7:"solbar;";i:9023;s:5:"sopf;";i:120164;s:7:"spades;";i:9824;s:10:"spadesuit;";i:9824;s:5:"spar;";i:8741;s:6:"sqcap;";i:8851;s:6:"sqcup;";i:8852;s:6:"sqsub;";i:8847;s:7:"sqsube;";i:8849;s:9:"sqsubset;";i:8847;s:11:"sqsubseteq;";i:8849;s:6:"sqsup;";i:8848;s:7:"sqsupe;";i:8850;s:9:"sqsupset;";i:8848;s:11:"sqsupseteq;";i:8850;s:4:"squ;";i:9633;s:7:"square;";i:9633;s:7:"squarf;";i:9642;s:5:"squf;";i:9642;s:6:"srarr;";i:8594;s:5:"sscr;";i:120008;s:7:"ssetmn;";i:8726;s:7:"ssmile;";i:8995;s:7:"sstarf;";i:8902;s:5:"star;";i:9734;s:6:"starf;";i:9733;s:16:"straightepsilon;";i:1013;s:12:"straightphi;";i:981;s:6:"strns;";i:175;s:4:"sub;";i:8834;s:5:"subE;";i:10949;s:7:"subdot;";i:10941;s:5:"sube;";i:8838;s:8:"subedot;";i:10947;s:8:"submult;";i:10945;s:6:"subnE;";i:10955;s:6:"subne;";i:8842;s:8:"subplus;";i:10943;s:8:"subrarr;";i:10617;s:7:"subset;";i:8834;s:9:"subseteq;";i:8838;s:10:"subseteqq;";i:10949;s:10:"subsetneq;";i:8842;s:11:"subsetneqq;";i:10955;s:7:"subsim;";i:10951;s:7:"subsub;";i:10965;s:7:"subsup;";i:10963;s:5:"succ;";i:8827;s:11:"succapprox;";i:10936;s:12:"succcurlyeq;";i:8829;s:7:"succeq;";i:10928;s:12:"succnapprox;";i:10938;s:9:"succneqq;";i:10934;s:9:"succnsim;";i:8937;s:8:"succsim;";i:8831;s:4:"sum;";i:8721;s:5:"sung;";i:9834;s:5:"sup1;";i:185;s:4:"sup1";i:185;s:5:"sup2;";i:178;s:4:"sup2";i:178;s:5:"sup3;";i:179;s:4:"sup3";i:179;s:4:"sup;";i:8835;s:5:"supE;";i:10950;s:7:"supdot;";i:10942;s:8:"supdsub;";i:10968;s:5:"supe;";i:8839;s:8:"supedot;";i:10948;s:8:"suphsub;";i:10967;s:8:"suplarr;";i:10619;s:8:"supmult;";i:10946;s:6:"supnE;";i:10956;s:6:"supne;";i:8843;s:8:"supplus;";i:10944;s:7:"supset;";i:8835;s:9:"supseteq;";i:8839;s:10:"supseteqq;";i:10950;s:10:"supsetneq;";i:8843;s:11:"supsetneqq;";i:10956;s:7:"supsim;";i:10952;s:7:"supsub;";i:10964;s:7:"supsup;";i:10966;s:6:"swArr;";i:8665;s:7:"swarhk;";i:10534;s:6:"swarr;";i:8601;s:8:"swarrow;";i:8601;s:7:"swnwar;";i:10538;s:6:"szlig;";i:223;s:5:"szlig";i:223;s:7:"target;";i:8982;s:4:"tau;";i:964;s:5:"tbrk;";i:9140;s:7:"tcaron;";i:357;s:7:"tcedil;";i:355;s:4:"tcy;";i:1090;s:5:"tdot;";i:8411;s:7:"telrec;";i:8981;s:4:"tfr;";i:120113;s:7:"there4;";i:8756;s:10:"therefore;";i:8756;s:6:"theta;";i:952;s:9:"thetasym;";i:977;s:7:"thetav;";i:977;s:12:"thickapprox;";i:8776;s:9:"thicksim;";i:8764;s:7:"thinsp;";i:8201;s:6:"thkap;";i:8776;s:7:"thksim;";i:8764;s:6:"thorn;";i:254;s:5:"thorn";i:254;s:6:"tilde;";i:732;s:6:"times;";i:215;s:5:"times";i:215;s:7:"timesb;";i:8864;s:9:"timesbar;";i:10801;s:7:"timesd;";i:10800;s:5:"tint;";i:8749;s:5:"toea;";i:10536;s:4:"top;";i:8868;s:7:"topbot;";i:9014;s:7:"topcir;";i:10993;s:5:"topf;";i:120165;s:8:"topfork;";i:10970;s:5:"tosa;";i:10537;s:7:"tprime;";i:8244;s:6:"trade;";i:8482;s:9:"triangle;";i:9653;s:13:"triangledown;";i:9663;s:13:"triangleleft;";i:9667;s:15:"trianglelefteq;";i:8884;s:10:"triangleq;";i:8796;s:14:"triangleright;";i:9657;s:16:"trianglerighteq;";i:8885;s:7:"tridot;";i:9708;s:5:"trie;";i:8796;s:9:"triminus;";i:10810;s:8:"triplus;";i:10809;s:6:"trisb;";i:10701;s:8:"tritime;";i:10811;s:9:"trpezium;";i:9186;s:5:"tscr;";i:120009;s:5:"tscy;";i:1094;s:6:"tshcy;";i:1115;s:7:"tstrok;";i:359;s:6:"twixt;";i:8812;s:17:"twoheadleftarrow;";i:8606;s:18:"twoheadrightarrow;";i:8608;s:5:"uArr;";i:8657;s:5:"uHar;";i:10595;s:7:"uacute;";i:250;s:6:"uacute";i:250;s:5:"uarr;";i:8593;s:6:"ubrcy;";i:1118;s:7:"ubreve;";i:365;s:6:"ucirc;";i:251;s:5:"ucirc";i:251;s:4:"ucy;";i:1091;s:6:"udarr;";i:8645;s:7:"udblac;";i:369;s:6:"udhar;";i:10606;s:7:"ufisht;";i:10622;s:4:"ufr;";i:120114;s:7:"ugrave;";i:249;s:6:"ugrave";i:249;s:6:"uharl;";i:8639;s:6:"uharr;";i:8638;s:6:"uhblk;";i:9600;s:7:"ulcorn;";i:8988;s:9:"ulcorner;";i:8988;s:7:"ulcrop;";i:8975;s:6:"ultri;";i:9720;s:6:"umacr;";i:363;s:4:"uml;";i:168;s:3:"uml";i:168;s:6:"uogon;";i:371;s:5:"uopf;";i:120166;s:8:"uparrow;";i:8593;s:12:"updownarrow;";i:8597;s:14:"upharpoonleft;";i:8639;s:15:"upharpoonright;";i:8638;s:6:"uplus;";i:8846;s:5:"upsi;";i:965;s:6:"upsih;";i:978;s:8:"upsilon;";i:965;s:11:"upuparrows;";i:8648;s:7:"urcorn;";i:8989;s:9:"urcorner;";i:8989;s:7:"urcrop;";i:8974;s:6:"uring;";i:367;s:6:"urtri;";i:9721;s:5:"uscr;";i:120010;s:6:"utdot;";i:8944;s:7:"utilde;";i:361;s:5:"utri;";i:9653;s:6:"utrif;";i:9652;s:6:"uuarr;";i:8648;s:5:"uuml;";i:252;s:4:"uuml";i:252;s:8:"uwangle;";i:10663;s:5:"vArr;";i:8661;s:5:"vBar;";i:10984;s:6:"vBarv;";i:10985;s:6:"vDash;";i:8872;s:7:"vangrt;";i:10652;s:11:"varepsilon;";i:949;s:9:"varkappa;";i:1008;s:11:"varnothing;";i:8709;s:7:"varphi;";i:966;s:6:"varpi;";i:982;s:10:"varpropto;";i:8733;s:5:"varr;";i:8597;s:7:"varrho;";i:1009;s:9:"varsigma;";i:962;s:9:"vartheta;";i:977;s:16:"vartriangleleft;";i:8882;s:17:"vartriangleright;";i:8883;s:4:"vcy;";i:1074;s:6:"vdash;";i:8866;s:4:"vee;";i:8744;s:7:"veebar;";i:8891;s:6:"veeeq;";i:8794;s:7:"vellip;";i:8942;s:7:"verbar;";i:124;s:5:"vert;";i:124;s:4:"vfr;";i:120115;s:6:"vltri;";i:8882;s:5:"vopf;";i:120167;s:6:"vprop;";i:8733;s:6:"vrtri;";i:8883;s:5:"vscr;";i:120011;s:8:"vzigzag;";i:10650;s:6:"wcirc;";i:373;s:7:"wedbar;";i:10847;s:6:"wedge;";i:8743;s:7:"wedgeq;";i:8793;s:7:"weierp;";i:8472;s:4:"wfr;";i:120116;s:5:"wopf;";i:120168;s:3:"wp;";i:8472;s:3:"wr;";i:8768;s:7:"wreath;";i:8768;s:5:"wscr;";i:120012;s:5:"xcap;";i:8898;s:6:"xcirc;";i:9711;s:5:"xcup;";i:8899;s:6:"xdtri;";i:9661;s:4:"xfr;";i:120117;s:6:"xhArr;";i:10234;s:6:"xharr;";i:10231;s:3:"xi;";i:958;s:6:"xlArr;";i:10232;s:6:"xlarr;";i:10229;s:5:"xmap;";i:10236;s:5:"xnis;";i:8955;s:6:"xodot;";i:10752;s:5:"xopf;";i:120169;s:7:"xoplus;";i:10753;s:7:"xotime;";i:10754;s:6:"xrArr;";i:10233;s:6:"xrarr;";i:10230;s:5:"xscr;";i:120013;s:7:"xsqcup;";i:10758;s:7:"xuplus;";i:10756;s:6:"xutri;";i:9651;s:5:"xvee;";i:8897;s:7:"xwedge;";i:8896;s:7:"yacute;";i:253;s:6:"yacute";i:253;s:5:"yacy;";i:1103;s:6:"ycirc;";i:375;s:4:"ycy;";i:1099;s:4:"yen;";i:165;s:3:"yen";i:165;s:4:"yfr;";i:120118;s:5:"yicy;";i:1111;s:5:"yopf;";i:120170;s:5:"yscr;";i:120014;s:5:"yucy;";i:1102;s:5:"yuml;";i:255;s:4:"yuml";i:255;s:7:"zacute;";i:378;s:7:"zcaron;";i:382;s:4:"zcy;";i:1079;s:5:"zdot;";i:380;s:7:"zeetrf;";i:8488;s:5:"zeta;";i:950;s:4:"zfr;";i:120119;s:5:"zhcy;";i:1078;s:8:"zigrarr;";i:8669;s:5:"zopf;";i:120171;s:5:"zscr;";i:120015;s:4:"zwj;";i:8205;s:5:"zwnj;";i:8204;}
    \ No newline at end of file
    diff --git a/mod/contacts.php b/mod/contacts.php
    new file mode 100644
    index 0000000..e6c2005
    --- /dev/null
    +++ b/mod/contacts.php
    @@ -0,0 +1,116 @@
    +argc != 3) || (! local_user()))
    +		return;
    +
    +	$contact_id = intval($a->argv[1]);
    +	if(! $contact_id)
    +		return;
    +
    +	$cmd = $a->argv[2];
    +
    +	$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +		intval($contact_id),
    +		intval($_SESSION['uid'])
    +	);
    +
    +	if(! count($r))
    +		return;
    +	$photo = str_replace('-4.jpg', '' , $r[0]['photo']);
    +	$photos = q("SELECT `id` FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d",
    +			dbesc($photo),
    +			intval($_SESSION['uid'])
    +	);
    +	
    +
    +	switch($cmd) {
    +		case 'edit':
    +				edit_contact($a,$contact_id);
    +			break;
    +		case 'block':
    +			$r = q("UPDATE `contact` SET `blocked` = 1 WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +				intval($contact_id),
    +				intval($_SESSION['uid'])
    +			);
    +			if($r)
    +				$_SESSION['sysmsg'] .= "Contact has been blocked." . EOL;
    +			break;
    +		case 'drop':
    +			$r = q("DELETE FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +				intval($contact_id),
    +				intval($_SESSION['uid']));
    +			if(count($photos)) {
    +				foreach($photos as $p) {
    +					q("DELETE FROM `photos` WHERE `id` = %d LIMIT 1",
    +						$p['id']);
    +				}
    +			}
    +			if($intval($contact_id))
    +				q("DELETE * FROM `item` WHERE `contact-id` = %d ",
    +					intval($contact_id)
    +				);
    +
    +			break;
    +		default:
    +			return;
    +			break;
    +	}
    +
    +}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +function contacts_content(&$a) {
    +	if(! local_user()) {
    +		$_SESSION['sysmsg'] .= "Permission denied." . EOL;
    +		return;
    +	}
    +
    +	if(($a->argc2 == 2) && ($a->argv[1] == 'all'))
    +		$sql_extra = '';
    +	else
    +		$sql_extra = " AND `blocked` = 0 ";
    +
    +	$tpl = file_get_contents("view/contacts-top.tpl");
    +	$o .= replace_macros($tpl,array(
    +		'$hide_url' => ((strlen($sql_extra)) ? 'contacts/all' : 'contacts' ),
    +		'$hide_text' => ((strlen($sql_extra)) ? 'Show Blocked Connections' : 'Hide Blocked Connections')
    +	)); 
    +
    +
    +	$r = q("SELECT * FROM `contact` WHERE `uid` = %d",
    +		intval($_SESSION['uid']));
    +
    +	if(count($r)) {
    +
    +		$tpl = file_get_contents("view/contact_template.tpl");
    +
    +		foreach($r as $rr) {
    +			if($rr['self'])
    +				continue;
    +			$o .= replace_macros($tpl, array(
    +				'$id' => $rr['id'],
    +				'$thumb' => $rr['thumb'], 
    +				'$name' => $rr['name'],
    +				'$url' => $rr['url']
    +			));
    +		}
    +	}
    +	return $o;
    +
    +
    +}
    \ No newline at end of file
    diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php
    new file mode 100644
    index 0000000..c830f1c
    --- /dev/null
    +++ b/mod/dfrn_confirm.php
    @@ -0,0 +1,374 @@
    +argc > 1)
    +		$node = $a->argv[1];
    +
    +	if(x($_POST,'source_url')) {
    +
    +	// We are processing an external confirmation to an introduction created by our user.
    +
    +		$public_key = $_POST['public_key'];
    +		$dfrn_id = $_POST['dfrn_id'];
    +		$source_url = $_POST['source_url'];
    +		$aes_key = $_POST['aes_key'];
    +
    +		if(intval($node)) 
    +			$r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
    +				intval($node));
    +		else
    +			$r = q("SELECT * FROM `user` WHERE `nickname` = '%s' LIMIT 1",
    +				dbesc($node));
    +
    +		if(! count($r)) {
    +			xml_status(3); // failure
    +		}
    +
    +		$my_prvkey = $r[0]['prvkey'];
    +		$local_uid = $r[0]['uid'];
    +
    +		$decrypted_source_url = "";
    +
    +		openssl_private_decrypt($source_url,$decrypted_source_url,$my_prvkey);
    +
    +
    +		$ret = q("SELECT * FROM `contact` WHERE `url` = '%s' AND `uid` = %d LIMIT 1",
    +			dbesc($decrypted_source_url),
    +			intval($local_uid));
    +
    +		if(! count($ret)) {
    +			// this is either a bogus confirmation or we deleted the original introduction.
    +			xml_status(3); 
    +		}
    +
    +		// Decrypt all this stuff we just received
    +
    +		$foreign_pubkey = $ret[0]['site-pubkey'];
    +		$dfrn_record = $ret[0]['id'];
    +		$decrypted_dfrn_id = "";
    +		openssl_public_decrypt($dfrn_id,$decrypted_dfrn_id,$foreign_pubkey);
    +
    +		if(strlen($aes_key)) {
    +			$decrypted_aes_key = "";
    +			openssl_private_decrypt($aes_key,$decrypted_aes_key,$my_prvkey);
    +			$dfrn_pubkey = openssl_decrypt($public_key,'AES-256-CBC',$decrypted_aes_key);
    +		}
    +		else {
    +			$dfrn_pubkey = $public_key;
    +		}
    +
    +		$r = q("SELECT * FROM `contact` WHERE `dfrn-id` = '%s' LIMIT 1",
    +			dbesc($decrypted_dfrn_id),
    +			intval($local_uid));
    +		if(count($r))
    +			xml_status(1); // Birthday paradox - duplicate dfrn-id
    +
    +		$r = q("UPDATE `contact` SET `dfrn-id` = '%s', `pubkey` = '%s' WHERE `id` = %d LIMIT 1",
    +			dbesc($decrypted_dfrn_id),
    +			dbesc($dfrn_pubkey),
    +			intval($dfrn_record));
    +		if($r) {
    +
    +			// We're good but now we have to scrape the profile photo and send notifications.
    +
    +			require_once("Photo.php");
    +
    +			$photo_failure = false;
    +
    +			$r = q("SELECT `photo` FROM `contact` WHERE `id` = %d LIMIT 1",
    +				intval($dfrn_record));
    +			if(count($r)) {
    +
    +				$filename = basename($r[0]['photo']);
    +				$img_str = fetch_url($r[0]['photo'],true);
    +				$img = new Photo($img_str);
    +				if($img) {
    +
    +					$img->scaleImageSquare(175);
    +					
    +					$hash = hash('md5',uniqid(mt_rand(),true));
    +
    +					$r = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`,
    +                                		`height`, `width`, `data`, `scale` )
    +                                		VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 4 )",
    +						intval($local_uid),
    +                                		dbesc($hash),
    +                                		datetime_convert(),
    +                                		datetime_convert(),
    +                                		dbesc(basename($r[0]['photo'])),
    +                                		intval($img->getHeight()),
    +						intval($img->getWidth()),
    +						dbesc($img->imageString())
    +					);
    +					if($r === false)
    +						$photo_failure = true;
    +					$img->scaleImage(80);
    +					$r =  q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`,
    +                                                `height`, `width`, `data`, `scale` )
    +                                                VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 5 )",
    +                                                intval($local_uid),
    +                                                dbesc($hash),
    +                                                datetime_convert(),
    +                                                datetime_convert(),
    +                                                dbesc(basename($r[0]['photo'])),
    +                                                intval($img->getHeight()),
    +                                                intval($img->getWidth()),
    +                                                dbesc($img->imageString())
    +                                        );
    +					if($r === false)
    +						$photo_failure = true;
    +
    +					$photo = $a->get_baseurl() . '/photo/' . $hash . '-4.jpg';
    +					$thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.jpg';
    +					
    +				}
    +				else
    +					$photo_failure = true;
    +			}
    +			else
    +				$photo_failure = true;
    +
    +			if($photo_failure) {
    +				$photo = $a->get_baseurl() . '/images/default-profile.jpg';
    +				$thumb = $a->get_baseurl() . '/images/default-profile-sm.jpg';
    +			}
    +
    +			$r = q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `blocked` = 0 WHERE `id` = %d LIMIT 1",
    +				dbesc($photo),
    +				dbesc($thumb),
    +				intval($dfrn_record)
    +			);
    +			if($r === false)
    +				$_SESSION['sysmsg'] .= "Unable to set contact photo info." . EOL;
    +
    +			// Otherwise everything seems to have worked and we are almost done. Yay!
    +			// Send an email notification
    +
    +			$r = q("SELECT * FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
    +				WHERE `contact`.`id` = %d LIMIT 1",
    +				intval($dfrn_record));
    +			
    +			$tpl = file_get_contents('view/intro_complete_eml.tpl');
    +			
    +			$email_tpl = replace_macros($tpl, array(
    +                                '$sitename' => $a->config['sitename'],
    +                                '$siteurl' =>  $a->get_baseurl(),
    +                                '$username' => $r[0]['username'],
    +                                '$email' => $r[0]['email'],
    +				'$fn' => $r[0]['name'],
    +				'$dfrn_url' => $r[0]['url'],
    +                                '$uid' => $newuid ));
    +
    +
    +                	$res = mail($r[0]['email'],"Introduction accepted at {$a->config['sitename']}",
    +				$email_tpl,"From: Administrator@{$_SERVER[SERVER_NAME]}");
    +			if(!$res) {
    +				$_SESSION['sysmsg'] .= "Email notification failed." . EOL;
    +			}
    +			xml_status(0); // Success
    +
    +			return; // NOTREACHED
    +
    +		}
    +		else
    +			xml_status(2);	// Hopefully temporary problem that can be retried.
    +
    +		return; // NOTREACHED
    +
    +	////////////////////// End of this scenario ///////////////////////////////////////////////
    +	}
    +	else {
    +
    +	// We are processing a local confirmation initiated on this system by our user to an external introduction.
    +
    +		$uid = $_SESSION['uid'];
    +
    +		if(! $uid) {
    +			$_SESSION['sysmsg'] = 'Unauthorised.';
    +			return;
    +		}	
    +	
    +		$dfrn_id = ((x($_POST,'dfrn_id')) ? notags(trim($_POST['dfrn_id'])) : "");
    +		$intro_id = intval($_POST['intro_id']);
    +
    +		$r = q("SELECT * FROM `contact` WHERE `issued-id` = '%s' AND `uid` = %d LIMIT 1",
    +				dbesc($dfrn_id),
    +				intval($uid)
    +				);
    +
    +		if((! $r) || (! count($r))) {
    +			$_SESSION['sysmsg'] = 'Node does not exist.' . EOL ;
    +			return;
    +		}
    +
    +		$contact_id = $r[0]['id'];
    +		$site_pubkey = $r[0]['site-pubkey'];
    +		$dfrn_confirm = $r[0]['confirm'];
    +		$aes_allow = $r[0]['aes_allow'];
    +
    +		$res=openssl_pkey_new(array(
    +        		'digest_alg' => 'whirlpool',
    +        		'private_key_bits' => 4096,
    +			'encrypt_key' => false ));
    +
    +
    +		$private_key = '';
    +
    +		openssl_pkey_export($res, $private_key);
    +
    +
    +		$pubkey = openssl_pkey_get_details($res);
    +		$public_key = $pubkey["key"];
    +
    +		$r = q("UPDATE `contact` SET `pubkey` = '%s', `prvkey` = '%s' WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +			dbesc($public_key),
    +			dbesc($private_key),
    +			intval($contact_id),
    +			intval($uid) 
    +			);
    +
    +
    +		$params = array();
    +
    +		$src_aes_key = random_string();
    +		$result = "";
    +
    +		openssl_private_encrypt($dfrn_id,$result,$a->user['prvkey']);
    +
    +		$params['dfrn_id'] = $result;
    +		$params['public_key'] = $public_key;
    +
    +
    +		openssl_public_encrypt($_SESSION['my_url'], $params['source_url'], $site_pubkey);
    +
    +		if($aes_allow && function_exists('openssl_encrypt')) {
    +			openssl_public_encrypt($src_aes_key, $params['aes_key'], $site_pubkey);
    +			$params['public_key'] = openssl_encrypt($public_key,'AES-256-CBC',$src_aes_key);
    +		}
    +
    +		$res = post_url($dfrn_confirm,$params);
    +
    +// uncomment the following two lines and comment the following xml/status lines
    +// to debug the remote confirmation section (when both confirmations 
    +// and responses originate on this system)
    +
    +// echo $res;
    +// $status = 0;
    +
    +		$xml = simplexml_load_string($res);
    +		$status = (int) $xml->status;
    +		switch($status) {
    +			case 0:
    +				$_SESSION['sysmsg'] .= "Confirmation completed successfully" . EOL;
    +				break;
    +			case 1:
    +
    +				// birthday paradox - generate new dfrn-id and fall through.
    +
    +				$new_dfrn_id = random_string();
    +				$r = q("UPDATE contact SET `issued-id` = '%s' WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +					dbesc($new_dfrn_id),
    +					intval($contact_id),
    +					intval($uid) 
    +				);
    +
    +			case 2:
    +				$_SESSION['sysmsg'] .= "Temporary failure. Please wait and try again." . EOL;
    +				break;
    +
    +
    +			case 3:
    +				$_SESSION['sysmsg'] .= "Introduction failed or was revoked. Cannot complete." . EOL;
    +				break;
    +		}
    +
    +		if(($status == 0 || $status == 3) && ($intro_id)) {
    +
    +			//delete the notification
    +
    +			$r = q("DELETE FROM `intro` WHERE `id` = %d AND `uid` = %d LIMIT 1",
    +				intval($intro_id),
    +				intval($uid)
    +			);
    +			
    +		}
    +		if($status != 0) 
    +			return;
    +		
    +
    +		require_once("Photo.php");
    +
    +		$photo_failure = false;
    +
    +		$r = q("SELECT `photo` FROM `contact` WHERE `id` = %d LIMIT 1",
    +			intval($contact_id));
    +		if(count($r)) {
    +
    +			$filename = basename($r[0]['photo']);
    +			$img_str = fetch_url($r[0]['photo'],true);
    +			$img = new Photo($img_str);
    +			if($img) {
    +
    +				$img->scaleImageSquare(175);
    +					
    +				$hash = hash('md5',uniqid(mt_rand(),true));
    +
    +				$r = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`,
    +                               		`height`, `width`, `data`, `scale` )
    +                               		VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 4 )",
    +					intval($local_uid),
    +                               		dbesc($hash),
    +                               		datetime_convert(),
    +                               		datetime_convert(),
    +                               		dbesc(basename($r[0]['photo'])),
    +                               		intval($img->getHeight()),
    +					intval($img->getWidth()),
    +					dbesc($img->imageString())
    +				);
    +				if($r === false)
    +					$photo_failure = true;
    +				$img->scaleImage(80);
    +				$r =  q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`,
    +					`height`, `width`, `data`, `scale` )
    +                                         VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 5 )",
    +                                         intval($local_uid),
    +                                         dbesc($hash),
    +                                         datetime_convert(),
    +                                         datetime_convert(),
    +                                         dbesc(basename($r[0]['photo'])),
    +                                         intval($img->getHeight()),
    +                                         intval($img->getWidth()),
    +                                         dbesc($img->imageString())
    +                                );
    +				if($r === false)
    +					$photo_failure = true;
    +
    +				$photo = $a->get_baseurl() . '/photo/' . $hash . '-4.jpg';
    +				$thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.jpg';
    +					
    +			}
    +			else
    +				$photo_failure = true;
    +		}
    +		else
    +			$photo_failure = true;
    +
    +		if($photo_failure) {
    +			$photo = $a->get_baseurl() . '/images/default-profile.jpg';
    +			$thumb = $a->get_baseurl() . '/images/default-profile-sm.jpg';
    +		}
    +
    +		$r = q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `blocked` = 0 WHERE `id` = %d LIMIT 1",
    +			dbesc($photo),
    +			dbesc($thumb),
    +			intval($contact_id)
    +		);
    +		if($r === false)
    +			$_SESSION['sysmsg'] .= "Unable to set contact photo info." . EOL;
    +	}
    +
    +	return;
    +}
    diff --git a/mod/dfrn_poll.php b/mod/dfrn_poll.php
    new file mode 100644
    index 0000000..e7f4b07
    --- /dev/null
    +++ b/mod/dfrn_poll.php
    @@ -0,0 +1,58 @@
    +config['dfrn_poll_dfrn_id'] = $_GET['dfrn_id'];
    +	if(x($_GET,'type'))
    +		$type = $a->config['dfrn_poll_type'] = $_GET['type'];
    +	if(x($_GET,'last_update'))
    +		$last_update = $a->config['dfrn_poll_last_update'] = $_GET['last_update'];
    +
    +
    +
    +	if(! x($dfrn_id))
    +		return;
    +
    +
    +	if((x($type)) && ($type == 'profile')) {
    +
    +		$r = q("SELECT `contact`.*, `user`.`nickname` 
    +			FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
    +			WHERE `issued-id` = '%s' LIMIT 1",
    +			dbesc($dfrn_id));
    +		if(count($r)) {
    +			$s = fetch_url($r[0]['poll'] . '?dfrn_id=' . $dfrn_id . '&type=profile-check');
    +			if(strlen($s)) {
    +				$xml = simplexml_load_string($s);
    +				if((int) $xml->status == 1) {
    +					$_SESSION['authenticated'] = 1;
    +					$_SESSION['visitor_id'] = $r[0]['id'];
    +					$_SESSION['sysmsg'] .= "Hi {$r[0]['name']}" . EOL;
    +					// Visitors get 1 day session.
    +					$session_id = session_id();
    +					$expire = time() + 86400;
    +					q("UPDATE `session` SET `expire` = '%s' WHERE `sid` = '%s' LIMIT 1",
    +						dbesc($expire),
    +						dbesc($session_id)); 
    +				}
    +			}
    +			$profile = ((strlen($r[0]['nickname'])) ? $r[0]['nickname'] : $r[0]['uid']);
    +			goaway($a->get_baseurl() . "/profile/$profile");
    +		}
    +		goaway($a->get_baseurl());
    +	}
    +
    +	if((x($type)) && ($type == 'profile-check')) {
    +
    +		q("DELETE FROM `expire` WHERE `expire` < " . time());
    +		$r = q("SELECT * FROM `profile_check` WHERE `dfrn_id` = '%s' ORDER BY `expire` DESC",
    +			dbesc($dfrn_id));
    +		if(count($r))
    +			xml_status(1);
    +		xml_status(0);
    +		return; // NOTREACHED
    +	}
    +
    +}
    diff --git a/mod/dfrn_request.php b/mod/dfrn_request.php
    new file mode 100644
    index 0000000..ef3c727
    --- /dev/null
    +++ b/mod/dfrn_request.php
    @@ -0,0 +1,290 @@
    +argc > 1)
    +		$which = $a->argv[1];
    +
    +	require_once('mod/profile.php');
    +	profile_init($a,$which);
    +
    +	return;
    +}}
    +
    +
    +if(! function_exists('dfrn_request_post')) {
    +function dfrn_request_post(&$a) {
    +
    +	if(($a->argc != 2) || (! count($a->profile)))
    +		return;
    +
    +
    +	if($_POST['cancel']) {
    +		goaway($a->get_baseurl());
    +	} 
    +
    +	// callback to local site after remote request and local confirm
    +
    +	if((x($_POST,'localconfirm')) && ($_POST['localconfirm'] == 1) 
    +		&& (x($_SESSION,'authenticated')) && (x($_SESSION,'uid'))
    +		&& ($_SESSION['uid'] == $a->argv[1]) && (x($_POST,'dfrn_url'))) {
    +
    +		$dfrn_url = notags(trim($_POST['dfrn_url']));
    +		$aes_allow = (((x($_POST,'aes_allow')) && ($_POST['aes_allow'] == 1)) ? 1 : 0);
    +		$confirm_key = ((x($_POST,'confirm_key')) ? $_POST['confirm_key'] : "");
    +		$failed = false;
    +
    +		require_once('Scrape.php');
    +
    +		if(x($dfrn_url)) {
    +
    +			$parms = scrape_dfrn($dfrn_url);
    +
    +			if(! count($parms)) {
    +				$_SESSION['sysmsg'] .= 'URL is not valid or does not contain profile information.' . EOL ;
    +				$failed = true;
    +			}
    +			else {
    +				if(! x($parms,'fn'))
    +					$_SESSION['sysmsg'] .= 'Warning: DFRN profile has no identifiable owner name.' . EOL ;
    +				if(! x($parms,'photo'))
    +					$_SESSION['sysmsg'] .= 'Warning: DFRN profile has no profile photo.' . EOL ;
    +				$invalid = validate_dfrn($parms);		
    +				if($invalid) {
    +					echo $invalid . ' required DFRN parameter' 
    +						. (($invalid == 1) ? " was " : "s were " )
    +						. "not found at the given URL" . '
    '; + + $failed = true; + } + } + } + if(! $failed) { + + $dfrn_request = $parms['dfrn-request']; + ///////////////////////// + dbesc_array($parms); + //////////////////////// + + $r = q("INSERT INTO `contact` ( `uid`, `created`,`url`, `name`, `photo`, `site-pubkey`, + `request`, `confirm`, `notify`, `poll`, `aes_allow`) + VALUES ( %d, '%s', '%s', '%s' , '%s', '%s', '%s', '%s', '%s', '%s', %d)", + intval($_SESSION['uid']), + datetime_convert(), + dbesc($dfrn_url), + $parms['fn'], + $parms['photo'], + $parms['key'], + $parms['dfrn-request'], + $parms['dfrn-confirm'], + $parms['dfrn-notify'], + $parms['dfrn-poll'], + intval($aes_allow) + ); + if($r === false) + $_SESSION['sysmsg'] .= "Failed to create contact." . EOL; + else + $_SESSION['sysmsg'] .= "Introduction complete."; + + // Allow the blocked remote notification to complete + + if(strlen($dfrn_request) && strlen($confirm_key)) + $s = fetch_url($dfrn_request . '?confirm_key=' . $confirm_key); + + goaway($dfrn_url); + } + } + + + // we are operating as a remote site and an introduction was requested of us. + // Scrape the originating DFRN-URL for everything we need. Create a contact record + // and an introduction to show our user next time he/she logs in. + // Finally redirect back to the originator so that their site can record the request. + // If our user confirms the request, a record of it will need to exist on the + // originator's site in order for the confirmation process to complete.. + + if($a->profile['nickname']) + $tailname = $a->profile['nickname']; + else + $tailname = $a->profile['uid']; + + $uid = $a->profile['uid']; + + $failed = false; + + require_once('Scrape.php'); + + if( x($_POST,'dfrn_url')) { + + $url = trim($_POST['dfrn_url']); + if(x($url)) { + $parms = scrape_dfrn($url); + + if(! count($parms)) { + $_SESSION['sysmsg'] .= 'URL is not valid or does not contain profile information.' . EOL ; + $failed = true; + } + else { + if(! x($parms,'fn')) + $_SESSION['sysmsg'] .= 'Warning: DFRN profile has no identifiable owner name.' . EOL ; + if(! x($parms,'photo')) + $_SESSION['sysmsg'] .= 'Warning: DFRN profile has no profile photo.' . EOL ; + $invalid = validate_dfrn($parms); + if($invalid) { + echo $invalid . ' required DFRN parameter' + . (($invalid == 1) ? " was " : "s were " ) + . "not found at the given URL" . '
    '; + + $failed = true; + } + } + } + + $ret = q("SELECT `url` FROM `contact` WHERE `url` = '%s'", dbesc($url)); + if($ret !== false && count($ret)) { + $_SESSION['sysmsg'] .= 'You have already introduced yourself here.' . EOL; + $failed = true; + } + + + if(! $failed) { + + $parms['url'] = $url; + $parms['issued-id'] = random_string(); + + ///////////////////////// + dbesc_array($parms); + //////////////////////// + + $ret = q("INSERT INTO `contact` ( `uid`, `created`, `url`, `name`, `issued-id`, `photo`, `site-pubkey`, + `request`, `confirm`, `notify`, `poll`, `visible` ) + VALUES ( %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d )", + intval($uid), + datetime_convert(), + $parms['url'], + $parms['fn'], + $parms['issued-id'], + $parms['photo'], + $parms['key'], + $parms['dfrn-request'], + $parms['dfrn-confirm'], + $parms['dfrn-notify'], + $parms['dfrn-poll'], + ((x($_POST,'visible')) ? 1 : 0 ) + ); + + } + if($ret === false) { + $_SESSION['sysmsg'] .= 'Failed to create contact record.' . EOL; + return; + } + + $ret = q("SELECT `id` FROM `contact` + WHERE `uid` = '%s' AND `url` = '%s' AND `issued-id` = '%s' + LIMIT 1", + intval($uid), + $parms['url'], + $parms['issued-id'] + ); + + if(($ret !== NULL) && (count($ret))) + $contact_id = $ret[0]['id']; + + $hash = random_string() . (string) time(); // Generate a confirm_key + + if($contact_id) { + $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `note`, `hash`, `datetime`) + VALUES ( %d, %d, 1, %d, '%s', '%s', '%s' )", + intval($uid), + intval($contact_id), + ((x($_POST,'knowyou') && ($_POST['knowyou'] == 1)) ? 1 : 0), + dbesc(trim($_POST['dfrn-request-message'])), + dbesc($hash), + dbesc(datetime_convert()) + ); + } + + + // TODO: send an email notification if our user wants one + + if(! $failed) + $_SESSION['sysmsg'] .= "Your introduction has been sent." . EOL; + + // "Homecoming" - send the requestor back to their site to record the introduction. + + $dfrn_url = bin2hex($a->get_baseurl() . "/profile/$tailname"); + $aes_allow = ((function_exists('openssl_encrypt')) ? 1 : 0); + + goaway($parms['dfrn-request'] . "?dfrn_url=$dfrn_url" . '&confirm_key=' . $hash . (($aes_allow) ? "&aes_allow=1" : "")); + + } + +}} + +if(! function_exists('dfrn_request_content')) { +function dfrn_request_content(&$a) { + + + + if(($a->argc != 2) || (! count($a->profile))) + return ""; + + $a->page['template'] = 'profile'; + + // "Homecoming". Make sure we're logged in to this site as the correct user. Then offer a confirm button + // to send us to the post section to record the introduction. + + if(x($_GET,'dfrn_url')) { + + if(! x($_SESSION,'authenticated')) { + $_SESSION['sysmsg'] .= "Please login to confirm introduction." . EOL; + return login(); + } + + // Edge case, but can easily happen in the wild. This person is authenticated, + // but not as the person who needs to deal with this request. + + if (($_SESSION['uid'] != $a->argv[1]) && ($a->user['nickname'] != $a->argv[1])) { + $_SESSION['sysmsg'] .= "Incorrect identity currently logged in. Please login to this profile." . EOL; + return login(); + } + + $dfrn_url = notags(trim(pack("H*" , $_GET['dfrn_url']))); + $aes_allow = (((x($_GET,'aes_allow')) && ($_GET['aes_allow'] == 1)) ? 1 : 0); + $confirm_key = (x($_GET,'confirm_key') ? $_GET['confirm_key'] : ""); + $o .= file_get_contents("view/dfrn_req_confirm.tpl"); + $o = replace_macros($o,array( + '$dfrn_url' => $dfrn_url, + '$aes_allow' => (($aes_allow) ? '' : "" ), + '$confirm_key' => $confirm_key, + '$username' => $a->user['username'], + '$uid' => $_SESSION['uid'], + 'dfrn_rawurl' => $_GET['dfrn_url'] + )); + return $o; + + } + else { + // safe to send our user their introduction + if((x($_GET,'confirm_key')) && strlen($_GET['confirm_key'])) { + $r = q("UPDATE `intro` SET `blocked` = 0 WHERE `hash` = '%s' LIMIT 1", + dbesc($_GET['confirm_key']) + ); + return; + } + + + // Outside request. Display our user's introduction form. + + + $o = file_get_contents("view/dfrn_request.tpl"); + $o = replace_macros($o,array('$uid' => $a->profile['uid'])); + return $o; + } +}} \ No newline at end of file diff --git a/mod/home.php b/mod/home.php new file mode 100644 index 0000000..44f2a98 --- /dev/null +++ b/mod/home.php @@ -0,0 +1,24 @@ +user['nickname']) + goaway( $a->get_baseurl() . "/profile/" . $a->user['nickname'] ); + else + goaway( $a->get_baseurl() . "/profile/" . $_SESSION['uid'] ); + } +}} + + +if(! function_exists('home_content')) { +function home_content(&$a) { + + $a->page['footer'] .= "
    Powered by DFRN
    "; + $o .= '

    Welcome' . ((x($a->config,'sitename')) ? " to {$a->config['sitename']}" : "" ) . '

    '; + $o .= login(1); + return $o; + + +}} \ No newline at end of file diff --git a/mod/item.php b/mod/item.php new file mode 100644 index 0000000..588dd9a --- /dev/null +++ b/mod/item.php @@ -0,0 +1,68 @@ +get_baseurl() . "/profile/$uid"); + + + + + + + +} \ No newline at end of file diff --git a/mod/login.php b/mod/login.php new file mode 100644 index 0000000..296890a --- /dev/null +++ b/mod/login.php @@ -0,0 +1,8 @@ +config['register_enabled']); + return login(1); +} \ No newline at end of file diff --git a/mod/notifications.php b/mod/notifications.php new file mode 100644 index 0000000..1064729 --- /dev/null +++ b/mod/notifications.php @@ -0,0 +1,98 @@ +get_baseurl()); + } + + $request_id = (($a->argc > 1) ? $a->argv[0] : 0); + + if($request_id == "all") + return; + + if($request_id) { + + $r = q("SELECT `id` FROM `intro` + WHERE `request-id` = %d + AND `uid` = %d LIMIT 1", + intval($request_id), + intval($_SESSION['uid']) + ); + + if(count($r)) { + $intro_id = $r[0]['id']; + } + else { + $_SESSION['sysmsg'] .= "Invalid request identifier." . EOL; + return; + } + if($_POST['submit'] == 'Discard') { + $r = q("DELETE `intro` WHERE `id` = %d LIMIT 1", intval($intro_id)); + $r = q("DELETE `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($request_id), + intval($_SESSION['uid'])); + return; + } + if($_POST['submit'] == 'Ignore') { + $r = q("UPDATE `intro` SET `ignore` = 1 WHERE `id` = %d LIMIT 1", + intval($intro_id)); + return; + } + } +} + + + + + +function notifications_content(&$a) { + + $o = ''; + + if((! x($_SESSION,'authenticated')) || (! (x($_SESSION,'uid')))) { + goaway($a->get_baseurl()); + } + + if(($a->argc > 1) && ($a->argv[1] == 'all')) + $sql_extra = ''; + else + $sql_extra = " AND `ignore` = 0 "; + + + $tpl = file_get_contents('view/intros-top.tpl'); + $o .= replace_macros($tpl,array( + '$hide_url' => ((strlen($sql_extra)) ? 'notifications/all' : 'notifications' ), + '$hide_text' => ((strlen($sql_extra)) ? 'Show Ignored Requests' : 'Hide Ignored Requests') + )); +dbg(2); + $r = q("SELECT `intro`.`id` AS `intro-id`, `intro`.*, `contact`.* + FROM `intro` LEFT JOIN `contact` ON `intro`.`contact-id` = `contact`.`id` + WHERE `intro`.`uid` = %d $sql_extra AND `intro`.`blocked` = 0 ", + intval($_SESSION['uid'])); +dbg(0); + if(($r !== false) && (count($r))) { + + + $tpl = file_get_contents("view/intros.tpl"); + + foreach($r as $rr) { + + $o .= replace_macros($tpl,array( + '$intro_id' => $rr['intro-id'], + '$dfrn-id' => $rr['issued-id'], + '$uid' => $_SESSION['uid'], + '$contact-id' => $rr['contact-id'], + '$photo' => ((x($rr,'photo')) ? $rr['photo'] : "images/default-profile.jpg"), + '$fullname' => $rr['name'], + '$knowyou' => (($rr['knowyou']) ? 'yes' : 'no'), + '$url' => $rr['url'], + '$note' => $rr['note'] + )); + } + } + else + $_SESSION['sysmsg'] .= "No notifications." . EOL; + + return $o; +} \ No newline at end of file diff --git a/mod/photo.php b/mod/photo.php new file mode 100644 index 0000000..bd0e415 --- /dev/null +++ b/mod/photo.php @@ -0,0 +1,25 @@ +argc != 2) { + killme(); + } + $resolution = 0; + $photo = $a->argv[1]; + $photo = str_replace('.jpg','',$photo); + if(substr($photo,-2,1) == '-') { + $resolution = intval(substr($photo,-1,1)); + $photo = substr($photo,0,-2); + } + $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' + AND `scale` = %d LIMIT 1", + dbesc($photo), + intval($resolution)); + if($r === NULL || (! count($r))) { + killme(); + } + header("Content-type: image/jpeg"); + echo $r[0]['data']; + +} \ No newline at end of file diff --git a/mod/profile.php b/mod/profile.php new file mode 100644 index 0000000..b37d648 --- /dev/null +++ b/mod/profile.php @@ -0,0 +1,136 @@ +error = 404; + return; + } + + $a->profile = $r[0]; + + $a->page['template'] = 'profile'; + + $a->page['title'] = $a->profile['name']; + + return; +}} + +function profile_init(&$a) { + + if($_SESSION['authenticated']) { + + // choose which page to show (could be remote auth) + + } + + if($a->argc > 1) + $which = $a->argv[1]; + else { + $_SESSION['sysmsg'] .= "No profile" . EOL ; + $a->error = 404; + return; + } + + profile_load($a,$which); + + $dfrn_pages = array('request', 'confirm', 'notify', 'poll'); + foreach($dfrn_pages as $dfrn) + $a->page['htmlhead'] .= "get_baseurl()."/dfrn_{$dfrn}/{$which}\" />\r\n"; +} + +function item_display($item,$template) { + + $o .= replace_macros($template,array( + '$id' => $item['item_id'], + '$profile_url' => $item['url'], + '$name' => $item['name'], + '$thumb' => $item['thumb'], + '$body' => bbcode($item['body']), + '$ago' => relative_date($item['created']) + )); + + + return $o; +} + + + +function profile_content(&$a) { + + require_once("include/bbcode.php"); + require_once('include/security.php'); + +// $tpl = file_get_contents('view/profile_tabs.tpl'); + + + if(can_write_wall($a,$a->profile['profile_uid'])) { + $tpl = file_get_contents('view/jot-header.tpl'); + + $a->page['htmlhead'] .= replace_macros($tpl, array('$baseurl' => $a->get_baseurl())); + + $tpl = file_get_contents("view/jot.tpl"); + $o .= replace_macros($tpl,array( + '$baseurl' => $a->get_baseurl(), + '$profile_uid' => $a->profile['profile_uid'] + )); + } + + + if($a->profile['is-default']) { + + // TODO left join with contact which will carry names and photos. (done)Store local users in contact as well as user.(done) + // Alter registration and settings + // and profile to update contact table when names and photos change. + // work on item_display and can_write_wall + + // Add comments. + + $r = q("SELECT `item`.*, `contact`.`name`, `contact`.`photo`, `contact`.`thumb`, `contact`.`id` AS `cid` + FROM `item` LEFT JOIN `contact` ON `contact`.`id` = `item`.`contact-id` + WHERE `item`.`uid` = %d AND `item`.`visible` = 1 + AND `contact`.`blocked` = 0 + AND `allow_uid` = '' AND `allow_gid` = '' AND `deny_uid` = '' AND `deny_gid` = '' + GROUP BY `item`.`parent`, `item`.`id` + ORDER BY `created` DESC LIMIT 0,30 ", + intval($a->profile['uid']) + ); + + $tpl = file_get_contents('view/wall_item.tpl'); + + if(count($r)) { + foreach($r as $rr) { + $o .= item_display($rr,$tpl); + } + } + } + + return $o; + + +} \ No newline at end of file diff --git a/mod/profile_photo.php b/mod/profile_photo.php new file mode 100644 index 0000000..f7e6825 --- /dev/null +++ b/mod/profile_photo.php @@ -0,0 +1,227 @@ +error = 404; + return; + } + require_once("mod/profile.php"); + profile_load($a,$_SESSION['uid']); +} + + +function profile_photo_post(&$a) { + + + + if((! x($_SESSION,'authenticated')) && (! (x($_SESSION,'uid')))) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + + if($a->argc > 1) + $profile_id = intval($a->argv[1]); + + if(x($_POST,'xstart') !== false) { + // phase 2 - we have finished cropping + if($a->argc != 3) { + $_SESSION['sysmsg'] .= "Image uploaded but image cropping failed." . EOL; + return; + } + $image_id = $a->argv[2]; + if(substr($image_id,-2,1) == '-') { + $scale = substr($image_id,-1,1); + $image_id = substr($image_id,0,-2); + } + + + $srcX = $_POST['xstart']; + $srcY = $_POST['ystart']; + $srcW = $_POST['xfinal'] - $srcX; + $srcH = $_POST['yfinal'] - $srcY; + + $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d LIMIT 1", + dbesc($image_id), + intval($scale)); + if($r !== NULL && (count($r))) { + $im = new Photo($r[0]['data']); + $im->cropImage(175,$srcX,$srcY,$srcW,$srcH); + $s = $im->imageString(); + $x = $im->getWidth(); + $y = $im->getHeight(); + + $ret = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`, + `height`, `width`, `data`, `scale` ) + VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 4 )", + intval($_SESSION['uid']), + dbesc($r[0]['resource-id']), + datetime_convert(), + datetime_convert(), + dbesc($r[0]['filename']), + intval($y), + intval($x), + dbesc($s)); + if($r === NULL) + $_SESSION['sysmsg'] .= "Image size reduction (175) failed." . EOL; + + $im->scaleImage(80); + $s = $im->imageString(); + $x = $im->getWidth(); + $y = $im->getHeight(); + $ret = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`, + `height`, `width`, `data`, `scale` ) + VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 5 )", + intval($_SESSION['uid']), + dbesc($r[0]['resource-id']), + datetime_convert(), + datetime_convert(), + dbesc($r[0]['filename']), + intval($y), + intval($x), + dbesc($s)); + if($r === NULL) + $_SESSION['sysmsg'] .= "Image size reduction (80) failed." . EOL; + $r = q("UPDATE `profile` SET `photo` = '%s', `thumb` = '%s' WHERE `id` = %d LIMIT 1", + dbesc($a->get_baseurl() . '/photo/' . $image_id . '-4.jpg'), + dbesc($a->get_baseurl() . '/photo/' . $image_id . '-5.jpg'), + intval($profile_id)); + if($r === NULL) + $_SESSION['sysmsg'] .= "Failed to add image to profile." . EOL; + + } + goaway($a->get_baseurl() . '/profiles'); + } + + $extra_sql = (($profile_id) ? " AND `id` = " . intval($profile_id) : " AND `is-default` = 1 " ); + + + $r = q("SELECT `id` FROM `profile` WHERE `uid` = %d $extra_sql LIMIT 1", intval($_SESSION['uid'])); + if($r === NULL || (! count($r))) { + $_SESSION['sysmsg'] .= "Profile unavailable." . EOL; + return; + } + + $src = $_FILES['userfile']['tmp_name']; + $filename = basename($_FILES['userfile']['name']); + $filesize = intval($_FILES['userfile']['size']); + + $imagedata = @file_get_contents($src); + $ph = new Photo($imagedata); + + if(! ($image = $ph->getImage())) { + $_SESSION['sysmsg'] .= "Unable to process image." . EOL; + @unlink($src); + return; + } + + @unlink($src); + $width = $ph->getWidth(); + $height = $ph->getHeight(); + + if($width < 175 || $width < 175) { + $ph->scaleImageUp(200); + $width = $ph->getWidth(); + $height = $ph->getHeight(); + } + + $hash = hash('md5',uniqid(mt_rand(),true)); + + $str_image = $ph->imageString(); + $smallest = 0; + $r = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`, + `height`, `width`, `data`, `scale` ) + VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 0 )", + intval($_SESSION['uid']), + dbesc($hash), + datetime_convert(), + datetime_convert(), + dbesc(basename($filename)), + intval($height), + intval($width), + dbesc($str_image)); + if($r) + $_SESSION['sysmsg'] .= "Image uploaded successfully." . EOL; + else + $_SESSION['sysmsg'] .= "Image upload failed." . EOL; + + if($width > 640 || $height > 640) { + $ph->scaleImage(640); + $str_image = $ph->imageString(); + $width = $ph->getWidth(); + $height = $ph->getHeight(); + + $r = q("INSERT INTO `photo` ( `uid`, `resource-id`, `created`, `edited`, `filename`, + `height`, `width`, `data`, `scale` ) + VALUES ( %d, '%s', '%s', '%s', '%s', %d, %d, '%s', 1 )", + intval($_SESSION['uid']), + dbesc($hash), + datetime_convert(), + datetime_convert(), + dbesc(basename($filename)), + intval($height), + intval($width), + dbesc($str_image)); + if($r === NULL) + $_SESSION['sysmsg'] .= "Image size reduction (640) failed." . EOL; + else + $smallest = 1; + } + + $a->config['imagecrop'] = $hash; + $a->config['imagecrop_resolution'] = $smallest; + $a->page['htmlhead'] .= file_get_contents("view/crophead.tpl"); + +} + + +if(! function_exists('profile_photo_content')) { +function profile_photo_content(&$a) { + + + if(! x($a->config,'imagecrop')) { + if((! x($_SESSION['authenticated'])) && (! (x($_SESSION,'uid')))) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + + if($a->argc > 1) + $profile_id = intval($a->argv[1]); + + $extra_sql = (($profile_id) ? " AND `id` = $profile_id " : " AND `is-default` = 1 " ); + + + $r = q("SELECT `id` FROM `profile` WHERE `uid` = %d $extra_sql LIMIT 1", intval($_SESSION['uid'])); + if($r === NULL || (! count($r))) { + $_SESSION['sysmsg'] .= "Profile unavailable." . EOL; + return; + } + + $o = file_get_contents('view/profile_photo.tpl'); + + $o = replace_macros($o,array( + '$profile_id' => $r[0]['id'], + '$uid' => $_SESSION['uid'], + )); + + return $o; + } + else { + $filename = $a->config['imagecrop'] . '-' . $a->config['imagecrop_resolution'] . '.jpg'; + $resolution = $a->config['imagecrop_resolution']; + $o = file_get_contents("view/cropbody.tpl"); + $o = replace_macros($o,array( + '$filename' => $filename, + '$profile_id' => $a->argv[1], + '$resource' => $a->config['imagecrop'] . '-' . $a->config['imagecrop_resolution'], + '$image_url' => $a->get_baseurl() . '/photo/' . $filename + )); + + return $o; + } + + +}} \ No newline at end of file diff --git a/mod/profiles.php b/mod/profiles.php new file mode 100644 index 0000000..cba358a --- /dev/null +++ b/mod/profiles.php @@ -0,0 +1,190 @@ +argc > 1) && ($a->argv[1] != "new") && intval($a->argv[1])) { + $r = q("SELECT * FROM `profile` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($a->argv[1]), + intval($_SESSION['uid']) + ); + if(! count($r)) { + $_SESSION['sysmsg'] .= "Profile not found." . EOL; + return; + } + + $profile_name = notags(trim($_POST['profile_name'])); + if(! strlen($profile_name)) { + $a->$_SESSION['sysmsg'] .= "Profile Name is required." . EOL; + return; + } + + $name = notags(trim($_POST['name'])); + $gender = notags(trim($_POST['gender'])); + $address = notags(trim($_POST['address'])); + $locality = notags(trim($_POST['locality'])); + $region = notags(trim($_POST['region'])); + $postal_code = notags(trim($_POST['postal_code'])); + $country_name = notags(trim($_POST['country_name'])); + $marital = notags(trim(implode(', ',$_POST['marital']))); + $homepage = notags(trim($_POST['homepage'])); + $about = str_replace(array('<','>','&'),array('<','>','&'),trim($_POST['about'])); + + if(! in_array($gender,array('','Male','Female','Other'))) + $gender = ''; + + $r = q("UPDATE `profile` + SET `profile-name` = '%s', + `name` = '%s', + `gender` = '%s', + `address` = '%s', + `locality` = '%s', + `region` = '%s', + `postal-code` = '%s', + `country-name` = '%s', + `marital` = '%s', + `homepage` = '%s', + `about` = '%s' + WHERE `id` = %d AND `uid` = %d LIMIT 1", + dbesc($profile_name), + dbesc($name), + dbesc($gender), + dbesc($address), + dbesc($locality), + dbesc($region), + dbesc($postal_code), + dbesc($country_name), + dbesc($marital), + dbesc($homepage), + dbesc($about), + intval($a->argv[1]), + intval($_SESSION['uid']) + ); + + if($r) + $_SESSION['sysmsg'] .= "Profile updated." . EOL; + } + + + +} + + + + +function profiles_content(&$a) { + if(! local_user()) { + $_SESSION['sysmsg'] .= "Unauthorised." . EOL; + return; + } + + if(($a->argc > 1) && ($a->argv[1] == 'new')) { + + $r0 = q("SELECT `id` FROM `profile` WHERE `uid` = %d", + intval($_SESSION['uid'])); + $num_profiles = count($r0); + + $name = "Profile-" . ($num_profiles + 1); + + $r1 = q("SELECT `name`, `photo`, `thumb` FROM `profile` WHERE `uid` = %d AND `is-default` = 1 LIMIT 1", + intval($_SESSION['uid'])); + + $r2 = q("INSERT INTO `profile` (`uid` , `profile-name` , `name`, `photo`, `thumb`) + VALUES ( %d, '%s', '%s', '%s', '%s' )", + intval($_SESSION['uid']), + dbesc($name), + dbesc($r1[0]['name']), + dbesc($r1[0]['photo']), + dbesc($ra[0]['thumb']) + ); + + $r3 = q("SELECT `id` FROM `profile` WHERE `uid` = %d AND `profile-name` = '%s' LIMIT 1", + intval($_SESSION['uid']), + dbesc($name) + ); + $_SESSION['sysmsg'] .= "New profile created." . EOL; + if(count($r3) == 1) + goaway($a->get_baseurl() . '/profiles/' . $r3[0]['id']); + goaway($a->get_baseurl() . '/profiles'); + } + + + if(intval($a->argv[1])) { + $r = q("SELECT * FROM `profile` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($a->argv[1]), + intval($_SESSION['uid']) + ); + if(! count($r)) { + $_SESSION['sysmsg'] .= "Profile not found." . EOL; + return; + } + + require_once('mod/profile.php'); + profile_load($a,$_SESSION['uid'],$r[0]['id']); + + require_once('view/profile_selectors.php'); + + $tpl = file_get_contents('view/jot-header.tpl'); + $profile_in_dir = file_get_contents("view/profile-in-directory.tpl"); + + $a->page['htmlhead'] .= replace_macros($tpl, array('$baseurl' => $a->get_baseurl())); + + $a->page['aside'] = file_get_contents('view/sidenote.tpl'); + $is_default = (($r[0]['is-default']) ? 1 : 0); + $tpl = file_get_contents("view/profile_edit.tpl"); + $o .= replace_macros($tpl,array( + '$baseurl' => $a->get_baseurl(), + '$profile_id' => $r[0]['id'], + '$profile_name' => $r[0]['profile-name'], + '$default' => (($is_default) ? "

    This is your public profile.

    " : ""), + '$name' => $r[0]['name'], + '$dob' => $r[0]['dob'], + '$address' => $r[0]['address'], + '$locality' => $r[0]['locality'], + '$region' => $r[0]['region'], + '$postal_code' => $r[0]['postal-code'], + '$country_name' => $r[0]['country-name'], + '$age' => $r[0]['age'], + '$gender' => gender_selector($r[0]['gender']), + '$marital' => marital_selector($r[0]['marital']), + '$about' => $r[0]['about'], + '$homepage' => $r[0]['homepage'], + '$profile_in_dir' => (($is_default) ? $profile_in_dir : '') + )); + + return $o; + + + } + else { + + $r = q("SELECT * FROM `profile` WHERE `uid` = %d", + $_SESSION['uid']); + if(count($r)) { + + $o .= file_get_contents('view/profile_listing_header.tpl'); + $tpl_default = file_get_contents('view/profile_entry_default.tpl'); + $tpl = file_get_contents('view/profile_entry.tpl'); + + foreach($r as $rr) { + $template = (($rr['is-default']) ? $tpl_default : $tpl); + $o .= replace_macros($template, array( + '$photo' => $rr['thumb'], + '$id' => $rr['id'], + '$profile_name' => $rr['profile-name'] + )); + } + } + return $o; + } + +} \ No newline at end of file diff --git a/mod/redir.php b/mod/redir.php new file mode 100644 index 0000000..ee15a18 --- /dev/null +++ b/mod/redir.php @@ -0,0 +1,21 @@ +argc == 2)) || (! intval($a->argv[1]))) + goaway($a->get_baseurl()); + $r = q("SELECT `dfrn-id`, `poll` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($a->argv[1]), + intval($_SESSION['uid'])); + if(! count($r)) + goaway($a->get_baseurl()); + q("INSERT INTO `profile_check` ( `uid`, `dfrn_id`, `expire`) + VALUES( %d, '%s', %d )", + intval($_SESSION['uid']), + dbesc($r[0]['dfrn-id']), + intval(time() + 30)); + goaway ($r[0]['poll'] . '?dfrn_id=' . $r[0]['dfrn-id'] . '&type=profile'); + + + +} \ No newline at end of file diff --git a/mod/register.php b/mod/register.php new file mode 100644 index 0000000..4e2c0bf --- /dev/null +++ b/mod/register.php @@ -0,0 +1,175 @@ +config['register_policy']) { + + + case REGISTER_OPEN: + $blocked = 0; + $verified = 1; + break; + + case REGISTER_VERIFY: + $blocked = 1; + $verify = 0; + break; + + default: + case REGISTER_CLOSED: + if((! x($_SESSION,'authenticated') && (! x($_SESSION,'administrator')))) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + $blocked = 0; + $verified = 0; + break; + } + + if(x($_POST,'username')) + $username = notags(trim($_POST['username'])); + if(x($_POST,'email')) + $email =notags(trim($_POST['email'])); + + if((! x($username)) || (! x($email))) { + $_SESSION['sysmsg'] .= "Please enter the required information.". EOL; + return; + } + + $err = ''; + + if(!eregi('[A-Za-z0-9._%-]+@[A-Za-z0-9._%-]+\.[A-Za-z]{2,6}',$email)) + $err .= " Not valid email."; + if(strlen($username) > 40) + $err .= " Please use a shorter name."; + if(strlen($username) < 3) + $err .= " Name too short."; + $r = q("SELECT `uid` FROM `user` + WHERE `email` = '%s' LIMIT 1", + dbesc($email) + ); + if($r !== false && count($r)) + $err .= " This email address is already registered." . EOL; + if(strlen($err)) { + $_SESSION['sysmsg'] .= $err; + return; + } + + + $new_password = autoname(6) . mt_rand(100,9999); + $new_password_encoded = hash('whirlpool',$new_password); + + $res=openssl_pkey_new(array( + 'digest_alg' => 'whirlpool', + 'private_key_bits' => 4096, + 'encrypt_key' => false )); + + // Get private key + + $prvkey = ''; + + openssl_pkey_export($res, $prvkey); + + // Get public key + + $pkey = openssl_pkey_get_details($res); + $pubkey = $pkey["key"]; + + $r = q("INSERT INTO `user` ( `username`, `password`, `email`, + `pubkey`, `prvkey`, `verified`, `blocked` ) + VALUES ( '%s', '%s', '%s', '%s', '%s', %d, %d )", + dbesc($username), + dbesc($new_password_encoded), + dbesc($email), + dbesc($pubkey), + dbesc($prvkey), + intval($verified), + intval($blocked) + ); + + if($r) { + $r = q("SELECT `uid` FROM `user` + WHERE `username` = '%s' AND `password` = '%s' LIMIT 1", + dbesc($username), + dbesc($new_password_encoded) + ); + if($r !== false && count($r)) + $newuid = intval($r[0]['uid']); + } + else { + $_SESSION['sysmsg'] .= "An error occurred during registration. Please try again." . EOL; + return; + } + + if(x($newuid) !== NULL) { + $r = q("INSERT INTO `profile` ( `uid`, `profile-name`, `is-default`, `name`, `photo`, `thumb` ) + VALUES ( %d, '%s', %d, '%s', '%s', '%s' ) ", + intval($newuid), + 'default', + 1, + dbesc($username), + dbesc($a->get_baseurl() . '/images/default-profile.jpg'), + dbesc($a->get_baseurl() . '/images/default-profile-sm.jpg') + ); + if($r === false) { + $_SESSION['sysmsg'] .= "An error occurred creating your default profile. Please try again." . EOL ; + // Start fresh next time. + $r = q("DELETE FROM `user` WHERE `uid` = %d", + intval($newuid)); + return; + } + $r = q("INSERT INTO `contact` ( `uid`, `created`, `self`, `name`, `photo`, `thumb`, `blocked` ) + VALUES ( %d, '%s', 1, '%s', '%s', '%s', 0 ) ", + intval($newuid), + datetime_convert(), + dbesc($username), + dbesc($a->get_baseurl() . '/images/default-profile.jpg'), + dbesc($a->get_baseurl() . '/images/default-profile-sm.jpg') + ); + + + } + + if( $a->config['register_policy'] == REGISTER_OPEN ) { + $email_tpl = file_get_contents("view/register_open_eml.tpl"); + $email_tpl = replace_macros($email_tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $username, + '$email' => $email, + '$password' => $new_password, + '$uid' => $newuid )); + + $res = mail($email,"Registration details for {$a->config['sitename']}",$email_tpl,"From: Administrator@{$_SERVER[SERVER_NAME]}"); + + } + + if($res) { + $_SESSION['sysmsg'] .= "Registration successful. Please check your email for further instructions." . EOL ; + goaway($a->get_baseurl()); + } + else { + $_SESSION['sysmsg'] .= "Failed to send email message. Here is the message that failed. $email_tpl " . EOL; + } + + return; +}} + + + + + + +if(! function_exists('register_content')) { +function register_content(&$a) { + + $o = file_get_contents("view/register.tpl"); + $o = replace_macros($o, array('$registertext' =>((x($a->config,'register_text'))? $a->config['register_text'] : "" ))); + return $o; + +}} + diff --git a/mod/settings.php b/mod/settings.php new file mode 100644 index 0000000..de1133f --- /dev/null +++ b/mod/settings.php @@ -0,0 +1,170 @@ +error = 404; + return; + } + require_once("mod/profile.php"); + profile_load($a,$_SESSION['uid']); +} + + +function settings_post(&$a) { + + if((! x($_SESSION['authenticated'])) && (! (x($_SESSION,'uid')))) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + if(count($a->user) && x($a->user,'uid') && $a->user['uid'] != $_SESSION['uid']) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + if((x($_POST,'password')) || (x($_POST,'confirm'))) { + + $newpass = trim($_POST['password']); + $confirm = trim($_POST['confirm']); + + $err = false; + if($newpass != $confirm ) { + $_SESSION['sysmsg'] .= "Passwords do not match. Password unchanged." . EOL; + $err = true; + } + + if((! x($newpass)) || (! x($confirm))) { + $_SESSION['sysmsg'] .= "Empty passwords are not allowed. Password unchanged." . EOL; + $err = true; + } + + if(! $err) { + $password = hash('whirlpool',$newpass); + $r = q("UPDATE `user` SET `password` = '%s' WHERE `uid` = %d LIMIT 1", + dbesc($password), + intval($_SESSION['uid'])); + if($r) + $_SESSION['sysmsg'] .= "Password changed." . EOL; + else + $_SESSION['sysmsg'] .= "Password update failed. Please try again." . EOL; + } + } + + $username = notags(trim($_POST['username'])); + $email = notags(trim($_POST['email'])); + if(x($_POST,'nick')) + $nick = notags(trim($_POST['nick'])); + $timezone = notags(trim($_POST['timezone'])); + + $username_changed = false; + $email_changed = false; + $nick_changed = false; + $zone_changed = false; + $err = ''; + + if($username != $a->user['username']) { + $username_changed = true; + if(strlen($username) > 40) + $err .= " Please use a shorter name."; + if(strlen($username) < 3) + $err .= " Name too short."; + } + if($email != $a->user['email']) { + $email_changed = true; + if(!eregi('[A-Za-z0-9._%-]+@[A-Za-z0-9._%-]+\.[A-Za-z]{2,6}',$email)) + $err .= " Not valid email."; + $r = q("SELECT `uid` FROM `user` + WHERE `email` = '%s' LIMIT 1", + dbesc($email) + ); + if($r !== NULL && count($r)) + $err .= " This email address is already registered." . EOL; + } + if((x($nick)) && ($nick != $a->user['nickname'])) { + $nick_changed = true; + if(! preg_match("/^[a-zA-Z][a-zA-Z0-9\-\_]*$/",$nick)) + $err .= " Nickname must start with a letter and contain only contain letters, numbers, dashes, and underscore."; + $r = q("SELECT `uid` FROM `user` + WHERE `nickname` = '%s' LIMIT 1", + dbesc($nick) + ); + if($r !== NULL && count($r)) + $err .= " Nickname is already registered. Try another." . EOL; + } + else + $nick = $a->user['nickname']; + + if(strlen($err)) { + $_SESSION['sysmsg'] .= $err . EOL; + return; + } + if($timezone != $a->user['timezone']) { + $zone_changed = true; + if(strlen($timezone)) + date_default_timezone_set($timezone); + } + if($email_changed || $username_changed || $nick_changed || $zone_changed ) { + $r = q("UPDATE `user` SET `username` = '%s', `email` = '%s', `nickname` = '%s', `timezone` = '%s' WHERE `uid` = %d LIMIT 1", + dbesc($username), + dbesc($email), + dbesc($nick), + dbesc($timezone), + intval($_SESSION['uid'])); + if($r) + $_SESSION['sysmsg'] .= "Settings updated." . EOL; + } + if($email_changed && $a->config['register_policy'] == REGISTER_VERIFY) { + + // FIXME - set to un-verified, blocked and redirect to logout + + } + + // Refresh the content display with new data + + $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", + intval($_SESSION['uid'])); + if(count($r)) + $a->user = $r[0]; +} + + +if(! function_exists('settings_content')) { +function settings_content(&$a) { + + if((! x($_SESSION['authenticated'])) && (! (x($_SESSION,'uid')))) { + $_SESSION['sysmsg'] .= "Permission denied." . EOL; + return; + } + + + $username = $a->user['username']; + $email = $a->user['email']; + $nickname = $a->user['nickname']; + $timezone = $a->user['timezone']; + + + if(x($nickname)) + $nickname_block = file_get_contents("view/settings_nick_set.tpl"); + else + $nickname_block = file_get_contents("view/settings_nick_unset.tpl"); + + $nickname_block = replace_macros($nickname_block,array( + '$nickname' => $nickname, + '$baseurl' => $a->get_baseurl())); + + $o = file_get_contents('view/settings.tpl'); + + $o = replace_macros($o,array( + '$baseurl' => $a->get_baseurl(), + '$uid' => $_SESSION['uid'], + '$username' => $username, + '$email' => $email, + '$nickname_block' => $nickname_block, + '$timezone' => $timezone, + '$zoneselect' => select_timezone($timezone) + )); + + return $o; + +}} \ No newline at end of file diff --git a/mod/test.php b/mod/test.php new file mode 100644 index 0000000..4fa625f --- /dev/null +++ b/mod/test.php @@ -0,0 +1,4 @@ +user); +} \ No newline at end of file diff --git a/nav.php b/nav.php new file mode 100644 index 0000000..c51c56a --- /dev/null +++ b/nav.php @@ -0,0 +1,23 @@ + +page['nav'] .= "\r\n"; + + if(x($_SESSION,'uid')) { + + $a->page['nav'] .= "Notifications\r\n"; + + $a->page['nav'] .= "Messages\r\n"; + + + $a->page['nav'] .= "Logout\r\n"; + + $a->page['nav'] .= "Settings\r\n"; + + $a->page['nav'] .= "Profiles\r\n"; + + $a->page['nav'] .= "Contacts\r\n"; + + $a->page['nav'] .= "Home\r\n"; + + } + $a->page['nav'] .= "\r\n\r\n"; diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/silho.gif b/silho.gif new file mode 100644 index 0000000000000000000000000000000000000000..048bdebc0b66c265d640001e5045c065963dfe70 GIT binary patch literal 79 zcmZ?wbhEHb6krfwSjfcC)z!7-(SHa~{K>+|#lXa%!vF*zc?Kqhp8k~w*4%hxyz#7- ccv;o{v!yrNeH+um)Rs7z2Yf0I;1ONB{r; literal 0 HcmV?d00001 diff --git a/silho.ico b/silho.ico new file mode 100644 index 0000000000000000000000000000000000000000..0c4676073b91b6b43612771b098ffb99b065e0be GIT binary patch literal 1150 zcmb`B!3l&g6hueZ0&?=~aogB#tR=O0v?3ePzsSHR%mh)DpGhX~iNFwVDTsB#`~gM) z-4r+4Fd(yxfsfnK`#t9y{l>J^7q1^VS(yeoy8Aw`3}+ literal 0 HcmV?d00001 diff --git a/tinymce/changelog.txt b/tinymce/changelog.txt new file mode 100644 index 0000000..bcd3f29 --- /dev/null +++ b/tinymce/changelog.txt @@ -0,0 +1,1075 @@ +Version 3.3.7 (2010-06-10) + Fixed bug where context menu would produce an error on IE if you right clicked twice and left clicked once. + Fixed bug where resizing of the window on WebKit browsers in fullscreen mode wouldn't position the statusbar correctly. + Fixed bug where IE would produce an error if the editor was empty and you where undoing to that initial level. + Fixed bug where setting the table background on gecko would produce \" entities inside the url style property. + Fixed bug where the button states wouldn't be updated correctly on IE if you placed the caret inside the new element. + Fixed bug where undo levels wasn't properly added after applying styles or font sizes. + Fixed bug where IE would throw an error if you used "select all" on empty elements and applied formatting to that. + Fixed bug where IE could select one extra character when you did a bookmark call on a caret location. + Fixed bug where IE could produce a script error on delete since it would sometimes produce an invalid DOM. + Fixed bug where IE would return the wrong start element if the whole element was selected. + Fixed bug where formatting states wasn't updated on IE if you pressed enter at the end of a block with formatting. + Fixed bug where submenus for the context menu wasn't removed correctly when the editor was destroyed. + Fixed bug where Gecko could select the wrong element after applying format to multiple elements. + Fixed bug where Gecko would delete parts of the previous element if the selection range was a element selection. + Fixed bug where Gecko would not merge paragraph elements correctly if they contained br elements. + Fixed bug where the cleanup button could produce span artifacts if you pressed it twice in a row. + Fixed bug where the fullpage plugin header/footer would be have it's header reseted to it's initial state on undo. + Fixed bug where an empty paragraph would be collapsed if you performed a cleanup while having the caret inside it. + Fixed a few memory leaks on IE especially with drop menus in listboxes and the spellchecker. + Fixed so formats applied to the current caret gets merged to reduce the number of output elements. + Added the latest version of Sizzle for the CSS selector logic to fix a compatibility issue with prototype. +Version 3.3.6 (2010-05-20) + Fixed bug where a editor.focus call could produce errors on IE in very specific scenarios. + Fixed bug where Gecko would produce an error if you unformatted text inside an empty element. + Fixed bug where IE would produce an error if the caret was placed before a table and you used the align buttons. + Fixed bug where the font size drop down didn't display the a preview correctly. + Fixed bug where the paste plugin wouldn't include all contents some times on WebKit browsers. + Fixed bug where the plain text mode toggle wouldn't work properly on WebKit. + Fixed bug where the editors statusbar would become invisible when you resized the window in fullscreen mode. +Version 3.3.5.1 (2010-05-07) + Fixed a critical bug with the fullscreen plugin. Produced error messages when the state was toggled on/off. +Version 3.3.5 (2010-05-06) + Added new merge_with_parents option to formats, enables the control of removal of elements with similar parents. + Fixed so the default behavior for applying classes isn't a toggle state but the old behavior from before the 3.3 release. + Fixed bug where selecting contents using double click on Gecko would produce errors when using removing format. + Fixed bug where the IE DOM could get messed up when non valid contents was pasted into the editor. + Fixed bug where merging selected table cells using the context menu didn't work as expected. + Fixed bug where some nestled formatting would be applied incorrectly. + Fixed bug with enter in list items when using the force_br_newlines mode on WebKit patch contributed by Ryan Koopmans. + Fixed bug where undo/redo could produce js errors on some specific operations. + Fixed bug where the theme_advanced_font_sizes didn't work as before 3.3 when complex settings where used. + Fixed bug where the table plugin would copy cell/row id attributes when making new rows/cells. +Version 3.3.4 (2010-04-27) + Fixed bug where fullscreen plugin would add two editor instances to EditorManager collection. + Fixed bug where it was difficult to enter text on non western languages such as Japanese on IE. + Fixed bug where removing contents from nodes could result in an exception when using undo/redo. + Fixed bug with selection of images inside layers or other resizable containers on IE. + Fixed so editors isn't initialized on iPhone/iPad devices since they don't have caret support. +Version 3.3.3 (2010-04-19) + Added new script_loaded callback function setting for the jQuery plugin. + Added various fixes and new rpc methods for the spellchecker plugin. Patch contributed by Michael Peters. + Removed some unnecessary inline style information from some of the dialogs. + Fixed some issues with the chaining for the TinyMCE jQuery plugin. + Fixed so any extra arguments passed to patched jQuery functions gets passed through. Patch contributed by Lee Henson. + Fixed so spellchecking/contextmenu can be toggled on/off if the browser has native spellchecker support. + Fixed bug where some texts in the new paste plugin wasn't placed in language pack. + Fixed bug where IE would produce an incorrect information message when cutting. + Fixed bug where removing items using the xhtmlxtras plugin wouldn't work correctly. + Fixed bug where setting table background images would add extra quotes on Gecko. + Fixed bug where shortcut for bold/italic/underline wouldn't work properly on WebKit. + Fixed bug where IE would produce an error message if only contents was an image tag and bold was used. + Fixed bug where the caret would move if alignment was applied to empty block elements. + Fixed bug where some shortcut key commands wouldn't apply formatting correctly. +Version 3.3.2 (2010-03-25) + Fixed bug where it was possible to scale the editor iframe smaller than the editor UI. + Fixed bug where some of the resizing option didn't work with the new live resize. + Fixed bug where the format listbox didn't show nestled formats correctly. + Fixed bug where the native listboxes didn't work correctly. + Fixed bug where font size selection in using the legacyoutput plugin would produce errors. + Fixed so block and blockquote formats remove their matching element regardless of it's attributes. +Version 3.3.1 (2010-03-18) + Added new live resize feature, the editor contents is now visible while resizing. + Fixed bug where some valid_element patterns would produce an unknown property error. + Fixed bug where it wasn't possible to toggle off blockquotes. + Fixed bug where an undo level wasn't produced when applying formatting using the styles dropdown. + Fixed bug where IE 6/7 wouldn't perform caret formatting due to a focus/event bug in IE. + Fixed bug where undo/redo wasn't restoring the previous selection correctly. + Fixed bug where the caret would become invisible if you resized the editor in latest Gecko. + Fixed bug where the class attribute wasn't completely removed in IE 6/7 when the removeClass function was used. + Fixed so the matchNode method of the Formatter class returns the matched format rule. + Fixed so it's possible to apply formatting to both blocks and as inline elements. +Version 3.3 (2010-03-10) + Fixed bug where backspace on a table on IE would produce an empty tbody and some JS exceptions. + Fixed bug where some redundant children wasn't removed properly when applying inline styles to them. + Fixed bug where Chrome would produce incorect dialog sizes if the inlinepopups plugin wasn't used. + Fixed bug where spans with different classes would get merged if they where siblings to each other. + Fixed bug where IE 8 would crash if you used the spellchecker. + Fixed bug where Input Method for non western languages didn't work correctly. + Fixed bug where the UI would render incorrectly in FF 3.6 on Mac due to a bug n their rendering engine. + Fixed bug where WebKit wouldn't scroll down correctly if Shift+Enter was used. Patch contributed by Thomas Andersen. +Version 3.3rc1 (2010-02-23) + Fixed bug with new legacyoutput plugin not working correctly on it's own. + Fixed bug some performance issues with removing text formats. + Fixed bug where TinyMCE specific attributes wasn't removed properly by remove format. + Fixed bug where it wasn't possible to align images within inline elements. + Fixed bug where Ctrl+Delete/Backspace would produce an invalid argument exception on IE. + Fixed bug where the search/replace logic could produce an infinite loop on IE for reverse searches. + Fixed bug where cloning formats in cells didn't work properly on IE. + Fixed bug where IE6 would produce a horizontal scroll bar. + Fixed so remove jQuery method removes the TinyMCE instance as well as the specified textarea. + Fixed so selected rows and cells gets updated using the row/cell properties dialogs. +Version 3.3b2 (2010-02-04) + Fixed bug where sometimes img elements would be removed by split method in DOMUtils. + Fixed bug where merging of span elements could occur on bookmark nodes. + Fixed bug where classes wasn't properly removed when removeformat was used on IE 6. + Fixed bug where multiple calls to an tinyMCE.init with mode set to exact could produce the same unique ID. + Fixed bug with the IE selection implementation when it was feeded an document range. + Fixed bug where block elements formatting wasn't properly removed by removeformat on all browsers. + Fixed bug where selection location was lost if you performed a manual cleanup. + Fixed bug where removeformat wouldn't remove span elements within styled block elements. + Fixed bug where an error would be thrown if you clicked on the separator lines in menus. + Fixed bug with the jQuery plugin adding always adding a querystring value to other resources. + Fixed bug where IE would produce an error message if you had an empty editor instance. + Fixed bug where Shift+Enter didn't produce br elements on WebKit browsers. + Fixed bug where a temporary marker element wasn't removed by the paste plugin. + Fixed bug where inserting a table would produce two undo levels instead of one. +Version 3.3b1 (2010-01-25) + Added new text formatting engine. Fixes a lot of browser quirks and adds new possibilities. + Added new advlist plugin that enables you to set the formats of list elements. + Added new paste plugin logic that enables you to retain style information from Office. + Added new autosave plugin logic that automatically saves contents in local storage. + Added new valid_styles option. Adds the possibility to restrict styles and their order. + Added new theme_advanced_runtime_fontsize option to display the runtime font size in font size select box. + Added new jquery plugin version that handles the gzip compressor amongst other things. Contributed by Speednet. + Added new $ function to tinymce namespace and editor instances for the jQuery build. + Added the possibility to get editors by index as well as name in the tinyMCE.editors collection. + Fixed so the contents inside the editor renders in standards mode by default. + Fixed bug where it wasn't possible to move the caret on short documents running in standards mode on IE. + Fixed bug where the decode method of the DOMUtils class could end up in an endless loop. + Fixed bug where it was possible to bypass the paste cleanup on non IE browsers if you clicked while pasting. + Fixed bug where some attributes wasn't serialized correctly on IE if wildcard attribute patters where used. + Fixed bug where entity decoding was performed on strings that didn't have any valid entities in them. + Fixed bugs with the insertNode method of the IE DOMRange implementation. Patch contributed by Scott McNaught. + Rewrote the getBookmark/moveToBookmark selection logic to boost performance on larger documents. + Rewrote the table plugin to include new cell selection logic and fixed various bugs and issues. + Merged the tinyMCE, tinymce and tinymce.EditorManager into the same instance makes more sense. + Removed browser setting since the browser support for TinyMCE is not far better than it was when that setting was introduced. + Changed the mce_ attribute prefix to the more standard _mce_ prefix. This is similar to browser vendors prefixes. + Optimized performance with named entities on Gecko. Regexp replace was executing very slowly probably due to a Gecko bug. + Optimized performance of the IE specific selection/range implementation. + Removed the safari plugin since we now replaced all text formatting logic to custom code. +Version 3.2.7 (2009-09-22) + Fixed bug where uppercase paragraphs could still produce an invalid DOM tree on IE. + Fixed bug where split command didn't work on WebKit since the node serializer needs a real document to work with. + Fixed bug where it was impossible in Gecko to place the caret before a table if it was the first one. + Fixed bug where linking to urls like ../../ would produce an extra traling slash ../..//. + Fixed bug where the template cdate functionality was using an old 2.x API call. Patch contributed by vectorjohn. + Fixed bug where urls to the same site but different protocol would be converted when relative_urls where set to false. Patch contributed by Ted Rust. + Fixed bug where the paste plugin would remove mceItem prefixed classes. + Fixed bug where the paste plugin would sometimes add items in a reverse order on WebKit. + Fixed bug where the paste buttons would present an error message on Gecko even if you changed user.js. Patch contributed by Todd (teeaykay). + Fixed bug where Opera would crash if you had tables incorrectly placed inside paragraphs. + Fixed bug where styles elements wasn't properly processed if you had bad input HTML. + Fixed bug where style attributes wasn't properly forced into a specific format. + Fixed bug and issues with boolean attributes like checked, nowrap etc. + Fixed bug where input elements could override attributes on form elements. + Fixed bug where script or style elements could get modified by the DOMUtils processHTML method. + Fixed bug where the selected attribute could get lost when force root blocks logic got executed on IE. Patch contributed by Attila Mezei-Horvati. + Fixed bug where getAttribs method didn't handle boolean attributes correctly on IE. + Fixed so the paste from word dialog is presented if you paste content on an IE with to restrictive security settings. + Fixed so the paste_strip_class_attributes option is set to none by default in the paste plugin. + Removed default border=0 on tables for the default value of valid_elements. +Version 3.2.6 (2009-08-19) + Added new wordcount plugin, this will display the number of typed words as you write. Contributed by Andrew Ozz. + Added new getNext and getPrev methods to DOM utils. These will return the first matching sibling. + Fixed bug where it was impossible to place the caret after a table on Gecko. It will now add a paragraph after tables. + Fixed bug where inline dialogs would fail if used in a window opened using a showModalDialog. Patch contributed by Derek Britt. + Fixed bug where IE could sometimes render a unknown runtime error on invalid input HTML. + Fixed bug where some incorrectly placed tables wouldn't be moved outside the paragraphs on IE. + Fixed bug where uppercase script/style element wouldn't be handled correctly and converted to valid lowercase. + Fixed bug where some WebKit versions on Mac OS X would produce issues with hidden select fields. + Fixed bug where the media plugin would fail on WebKit since the node wasn't properly imported to the right document. + Fixed bug where absolute URLs for the TinyMCE script using a base href element would cause loading problems in IE 6/7. + Fixed bug where pasting using the paste plugin wasn't possible on IE with to restrictive security settings. + Fixed bug where pasting of whitespace was impossible using the new custom paste method. + Fixed bug where pasting on some WebKit browsers would not work if you pasted specific contents due to a WebKit bug. + Fixed bug where doctypes with multiple lines would not be parsed correctly by the fullpage plugin. Patch contributed by Colin. + Fixed bug where the autoresize plugin would break the fullscreen functionality. + Fixed bug where tables would be chopped up running on IE using invalid contents and pasting paragraphs into a cell. + Fixed bug where the each method of jQuery build didn't iterate styleSheets. We now use the TinyMCE API one instead. + Fixed bug where auto switching to paragraphs after headers some times failed in Gecko. + Fixed so all editor options gets passed to the Serializer class. Patch contributed by Jasper Mattsson. + Fixed so script/style blocks isn't wrapped in paragraphs as other inline elements. + Fixed so the XHR requests sends the X-Requested-With HTTP header. + Fixed so the data url scheme is handled in the tinymce.util.URI class. + Changed inline documentation to use moxiedoc style comments. + Removed the compat2x plugin people should have upgraded to the 3.x API by now. 3.0 was released more then a year ago. + Re-added Gecko specific message for users who doesn't understand the security concept regarding paste. +Version 3.2.5 (2009-06-29) + Added new jQuery plugin for the jQuery specific package. This enables you to more easily load and use TinyMCE. + Added new autoresize plugin contributed by Peter Dekkers. This plugin will auto resize the editor to the size of the contents. + Fixed so all packages have the same directory structure. Previous releases had a different structure for the production package. + Fixed so the paste from word dialog forces the contents to be processed as word contents even if it's not. + Fixed so the jQuery build adapter build works. It's currently only excluding Sizzle. + Fixed so noscript element contents is retained during the editing process. + Fixed bug where the getBookmark method would need a "simple" string input when the documented way is a boolean. + Fixed bug where invalid contents could break the fix_table_elements logic. + Fixed bug where Sizzle specific attributes would be serialized if the valid_elements was set to *[*]. + Fixed bug where IE would produce an error if you specified a relative content_css and opened the paste dialog. + Fixed bug where pasting images on IE would produce broken images if they came from an external site. + Fixed bug where memory was leaked if you add/remove controls dynamically. Some event handlers wasn't removed properly. + Fixed bug where domain relaxing wasn't treated correctly if you added it after the TinyMCE script element. + Fixed bug where the activeEditor wasn't set to null if the last editor instance was removed. + Fixed bug where IE was leaking memory on the onbeforeunload event due to some recently introduced logic. Patch contributed by Options. + Fixed bug where inserting tables in Safari 4 didn't work due to a new WebKit bug where some element names are reserved. + Fixed bug where URLs having a :// value in the query string would make it absolute regardless of URL settings. + Fixed the WebKit specific bug where DOM Ranges would fail if the node wasn't attached to something in a different way. + Removed the auto_resize option and the resizeToContent method from the tinymce.Editor class. Use the new autoresize plugin instead. +Version 3.2.4.1 (2009-05-25) + Fixed bug where Gecko browsers would produce an extra space after for example strong when loaded from sub domains. + Fixed bug where script elements would be removed if they where placed inside a paragraph element. + Fixed bug where IE 8 would produce 1 item remaining when loading CSS files dynamically with an empty cache. + Fixed bug where bound events would be removed from other editor instances if a specific one was removed. + Fixed various bugs and issues with script and style elements inside the editor. + Fixed so all script contents gets wrapped in CDATA sections so that they can be parsed using a XML parser. + Fixed so it's impossible for elements marked as closed to have child nodes rendered in output. +Version 3.2.4 (2009-05-21) + Added new paste_remove_styles/paste_remove_styles_if_webkit option to paste plugin concept contributed by Hadrien Gardeur. + Added new functionality to paste plugin contributed by Scott Eade aka monkeybrain. + Added new paste_block_drop option to the paste plugin this is disabled by default and will block any drag/drop event. + Added new bind/unbind methods to DOMUtils these works like Event.add/Event.remove but is easier to access. + Added new paste_dialog_width/paste_dialog_height options to paste pluign. Enables you to change the dialog sizes. + Fixed bug on IE 8 where it would sometimes produce a "1 item remaining" status message that would never finish. + Fixed bug on Safari 4 beta that would produce DOM Range exceptions on the DOMUtils split method since the browser has a bug. + Fixed bug where the paste plugin could accidentally think that some word sentences was supposed to be list elements. + Fixed bug where paste plugin would produce one extra empty undo level on some browsers. + Fixed bug where spans wasn't produced correctly on new line when the keep_styles option was enabled. + Fixed bug where the caret would be placed at the beginning of contents in IE 8 if you selected colors from the color pickers. + Fixed so the Event class is a normal class instead of a static one. The tinymce.dom.Event is now a global instance of that class. + Fixed so internal events for instances gets removed when the DOMUtils instance is removed. + Fixed so preventDefault and stopPropagation methods can be used on the event object in all browsers. +Version 3.2.3.1 (2009-05-05) + Fixed bug where paragraphs containing form elements such as input or textarea would be removed. + Fixed bug where some IE versions would produce a wrapper function for events attributes. + Fixed bug where table cell contents could be removed if you pressed return/enter at the end of the cell contents. + Fixed bug where the paste plugin would remove a extra character if the selection range was collapsed. + Fixed bug where creating tables with % width wouldn't be handled correctly on WebKit browsers. +Version 3.2.3 (2009-04-23) + Added new paste plugin logic. This new version will autodetect Word contents and clean it up. + Added a optional root element argument to getPos so you can tell it where to stop the calculation. + Added new DOM ready logic to remove the usage of document.write. We now use basically the same method as jQuery. + Fixed bug where WebKit browsers would fail when selecting all contents in the area using Ctrl+A. + Fixed bug where IE would produce paragraphs with empty inline style elements. + Fixed bug where WebKit browsers would fail when inserting tables with a non pixel width. + Fixed bug where block elements could get a redundant br element at the end of the element. + Fixed bug where the tabfocus plugin only worked with a single editor instance on page. + Fixed bug where IE 8 was loosing caret position if the selection was collapsed and a menu was clicked. + Fixed bug with application/xhtml+xml mode where menus wasn't working properly. + Fixed bug where the onstop workaround fix for IE would produce errors in an ASP update panel. + Fixed bug where the submit function override could produce errors if executed in the wrong scope. + Fixed bug where the area element wasn't closed by a short ending. + Fixed various number issues in the style plugins properties dialog. Contributed by datpaulchen. + Fixed issues with size suffix values in the style plugin dialog. + Fixed issue where hasDuplicate variable would leak out to the global space due to a bug in the Sizzle engine. + Fixed issue where the paste event would fire a dialog warning on IE since we extracted the text contents. + Updated Sizzle engine to the latest version, this version fixes a few bugs that was reported. +Version 3.2.2.3 (2009-03-26) + Fixed regression bug with the getPos method, it would return invalid if the view port was to small. +Version 3.2.2.2 (2009-03-25) + Fixed so the DOMUtils getPos method can be used cross documents if needed. + Fixed bug where undo/redo wasn't working correctly in Gecko browsers. +Version 3.2.2.1 (2009-03-19) + Added support for tel: URL prefixes. Even though this doesn't match any official RFC. + Fixed so the select method of the Selection class selects the first best suitable contents. + Fixed bug where the regexps for www. prefixes for link and advlink dialogs would match wwwX. + Fixed bug where the preview dialog would fail to open if the content_css wasn't defined. Patch contributed by David Bildström (ChronoZ). + Fixed bug where editors wasn't converted in application/xhtml+xml mode due to an issue with Sizzle. + Fixed bug where alignment would fail if multiple lines where selected. + Updated Sizzle engine to the latest version, this version fixes a few bugs that was reported. +Version 3.2.2 (2009-03-05) + Added new CSS selector engine. Sizzle the same one that jQuery and other libraries are using. + Added new is and getParents methods to the DOMUtils class. These use the new Sizzle engine to select elements. + Added new removeformat_selector option, enables you to specify a CSS selector pattern of elements to remove when using removeformat. + Fixed so the getParent method can take CSS expressions when selecting it's parents. + Added new ant based build process, includes a new javabased preprocessor and a yuicompressor ant task. + Moved the tab_focus logic into a plugin called tabfocus, so the old tab_focus option has been removed from the core. + Replaced the TinyMCE custom unit testing framework with Qunit and rewrote all tests to match the new logic. + Moved the examples/testcases to a root directory called tests since it now includes slickspeed. + Fixed bug where nbsp wasn't replaced correctly in ForceBlocks.js. Patch contributed by thorn. + Fixed bug where an dom exception would be thrown in Gecko when the theme_advanced_path path was set to false under xml application mode. + Fixed bug where it was impossible to get out of a link at the end of a block element in Gecko. + Fixed bug where the latest WebKit nightly would fail when changing font size and font family. + Fixed bug where the latest WebKit nightly would fail when opening dialogs due to changes to the arguments object. + Fixed bug where paragraphs wasn't added to elements positioned absolute using classes. + Fixed bug where font size values with dot's like 1.4em would produce a class instead of the style value. + Fixed bug where IE 8 would return an incorrect position for elements. + Fixed bug where IE 8 would render colorpicker/filepicker icons incorrectly. + Fixed bug where trailing slashes for directories in URLs would be removed. + Fixed bug where autostart and other boolean values in the media dialog wouldn't be stored/parsed correctly. + Fixed bug where the repaint call for the media plugin wouldn't be executed due to a typo in the source. + Fixed bug where id attribute of object elements wasn't kept intact by the media plugin. + Fixed bug where preview of embeded elements when the media_use_script option was used would fail. + Fixed bug where inlinepopups could be rendered at an incorrect location on IE 6 while dragging. + Fixed bug where the blocker shim could be placed at an incorrect location on IE 6. + Fixed bug where the multiple and size attributes of select elements would produce incorrect values while running in IE. + Fixed bug where IE would loose the caret position is you selected a color from the color drop down. + Fixed bug where remove format wouldn't work on IE since it couldn't remove span elements that had style information. + Fixed bug where Opera was removing links when removing formatting from selected contents. + Fixed bug where paragraphs could be produced inside non positional elements styled with the CSS position value of static. + Fixed bug where removeformat wouldn't work if you selected part of a span in IE. + Fixed bug where media plugin didn't retain the style attribute on embed/object elements. + Fixed bug where auto focus on empty editor instances could produce strange results if you inserted an image into it. + Fixed bug where   characters would be removed in FF when inserted with the mceInsertContent or selection.setContent methods. + Fixed bug where warning message of missing paste support wasn't displayed on WebKit browsers. + Fixed bug where anchor links could include other links. The selected range is now unlinked before adding news links to it. + Fixed memory leak when TinyMCE was used with prototype. Patch contributed by James Ots. + Fixed so the non documented fullpage_hide_in_source_view option for the fullpage plugin works again in the 3.x branch. + Fixed so tables doesn't get inserted into paragraphs by default since it's not W3C valid. Can be disabled by using the fix_table_elements option. + Fixed so the source view dialog sets a source_view state to the event object. Enables plugins to intercept the source view mode. + Fixed various validation issues with the html dialogs and pages. + Removed ask mode option since there is way better ways of doing this now. Use the add/remove control methods instead. + Removed logic for compatibility with Safari 2.x, this browser is no longer supported since no one is using it. + Removed the auto domain relaxing feature. If loading scripts cross sub domains it's better to specify the document.domain by hand. +Version 3.2.1.1 (2008-11-27) + Added new theme_advanced_default_background_color/theme_advanced_default_foreground_color options. Patch contributed by David Bildström (ChronoZ). + Fixed font style formatting compatibility issue with Adobe Air. + Fixed so legacy font elements get converted into spans even if cleanup_on_startup isn't enabled. + Fixed bug where pre elements could be incorrectly modified by an IE bug workaround. Patch contributed by hu vime. + Fixed bug where input elements inside inlinepopups wasn't editable in Firefox 2. + Fixed bug where the xhtmlxtras plugin wasn't replacing attribute values correctly. + Fixed bug where menu buttons in skin variants would look strange due to IE 8 fixes. + Fixed bug where WebKit browsers would on backspace take you back to the previous page if the editor was empty. + Fixed bug where DOMUtils decode method wouldn't handle strings larger than 4096kb due to node chunking. + Fixed bug where meta key wasn't handled as ctrl key on Mac OS X for custom keyboard short cuts. + Fixed bug where init event would get fired twice on WebKit on Mac OS X. +Version 3.2.1 (2008-11-04) + Added support for custom icon image for drop menus. Use icon_src to set a custom image directly. + Added new media_strict option to media plugin. Enables you to control if the flash embed is strict or not. Enabled by default. + Fixed so the editors script files gets dynamically loaded without using XHR or eval. + Fixed so the media plugin outputs valid XHTML object elements for Flash movies. Can be disabled with the media_strict option. + Fixed so dynamic loading doesn't require eval calls on non IE browsers for better Air support. + Fixed bug where the editor wasn't treated as empty if the remaining paragraph had attributes. + Fixed bug where id's of elements was removed ones they got wrapped in paragraphs. Patch contributed by ChronoZ. + Fixed bug where WebKit browsers where placing list elements inside paragraph elements. + Fixed bug where inserting images or links would produce absolute urls on WebKit browsers. + Fixed bug where values for checked, readonly, disabled and selected attributes was incorrect on IE. + Fixed bug where positive values for checked, readonly, disabled and selected attributes wasn't forced to valid values. + Fixed bug where selecting the first option in a native select box would produce an undefined error. + Fixed bug where tabindex 32768 could be outputted on IE if element attributes where cloned. + Fixed bug where the media dialogs preview window would display incorrect contents due to duplicate clsid prefixes. + Fixed bug where non pixel or percent heights for textarea elements would produce errors on IE. + Fixed bug where cdata sections in script elements wasn't handled correctly. + Fixed bug where nowrap of table cells would produce a 65535 value output. + Fixed bug where media plugin would produce an error if you selected the first item in the items list. + Fixed bug where media plugin would modify links with the item _value in them. + Fixed so table width/height is better forced if inline_styles is enabled. Patch contributed by daKmoR. + Fixed css for IE 8 such as opacity and other rendering quirks. +Version 3.2.0.2 (2008-10-02) + Fixed bug where the SelectBox and NativeSelectBox wasn't updated correctly if undefined was passed to them. + Fixed bug where the style dropdown wasn't correctly changed back to it's original state when element had no class. + Fixed bug where multiple pending font styles wasn't handled correctly. + Fixed so you can disable all auto css loading for dialogs by setting the popups_css option to false. +Version 3.2.0.1 (2008-09-17) + Fixed bug where font sizes and faces wouldn't be changed correctly when there was a parent with a different style. + Fixed bug where adding fonts to the same selection would produce redundant spans. +Version 3.2 (2008-09-11) + Added new text style support, it will now use span elements internally instead of font elements. + Added new improved support for the theme_advanced_font_sizes option, check the Wiki for details. + Added new keep_style setting that maintains the text style on return/enter on non IE browsers, enabled by default. + Added new onBeforeSetContent/onBeforeGetContent/onSetContent/onGetContent events to the Selection class. + Added new selectByIndex method to ListBox class. This enables you to select list items by an index instead of a value. + Added new possibility to the select method of the ListBox class. This can now have a selector function as it's value argument. + Added new possibility to skip the loading of popups css by setting the feature popup_css to the value false. + Added new possibility to skip translation of popups by setting the translate_i18n feature to false. + Added new element_format option enables you to produce HTML element endings instead of XHTML. But we are still in the XHTML is better camp. + Added missing allowfullscreen and quality options for flash elements, this will now get correctly stored. + Fixed bug where table cell dialog didn't close properly unless the accessibility_warnings option was set to false. + Fixed bug where the modal dialog blocker element for inlinepopups wasn't placed at a correct location if the page had scroll. + Fixed bug where non inline dialogs didn't close correctly if the inlinepopups plugin was used. + Fixed bug where non inline dialogs could make the modal dialog blocker to work incorrectly. + Fixed bug where style select wasn't populated correctly if you pressed the arrow. Patch by Hari Karam Singh. + Fixed bug where toggling the fullscreen mode didn't restore scrollbars on IE when the editor was inside a frame. Patch by Jacob Barrett. + Fixed bug where inserting flash contents using the template plugin didn't work correctly. + Fixed bug where inserting flash contents using the selection.setContent or mceInsertContent command didn't work correctly. + Fixed bug where IE would produce an exception if a comment started with -. + Fixed bug where the blockquote button would wrap lists incorrectly on non IE browsers. + Fixed bug where Opera would display BR elements in the element path. + Fixed bug where xhtmlxtras didn't insert elements correctly on IE. + Fixed bug where the buttons wasn't activated correctly in the xhtmlxtras plugin. + Fixed bug where adding an object as the style attribute for the dom setAttribs method wouldn't work. + Fixed bug where the background color would bleed out to parent container element in Gecko. + Fixed bug where the insert column actions for tables would fail if you did it in a thead or tfoot. Patch contributed by T Andersen (tan73). + Fixed bug where event blocker element wasn't positioned correctly for the inlinepopups plugin. + Fixed bug where pasting from Office 2007 would produce an odd comment in the contents. + Fixed bug where the paste as plain text could remove an extra character. Patch contributed by Speednet. + Fixed bug where some characters where missing for the paste_replace_list option. Patch contributed by Speednet. + Fixed bug where removing non existing editor instances by the mceRemoveControl command would produce an error. + Fixed bug where meta elements with the name description would produce errors in IE. + Fixed bug where color and background colors wouldn't be updated properly. + Fixed bug where the createMenuButton of tinymce.ControlManager didn't implement the last class argument. + Fixed bug where the editor_css option was relative from the TinyMCE installation directory not the current page. + Fixed bug where elements wouldn't be padded if the element contained bogus br elements. For example TD elements. + Fixed bug where parsing of in fullpage plugin would produce an error. + Fixed bug where relative urls with just ./ would become an empty string. + Fixed bug where outdent button would be disabled if inline_styles where set to false. + Fixed bug where replace with an empty search string would produce an error on IE. + Fixed bug where restoring the overflow state of the body in fullscreen plugin running on IE would produce vertical scrollbars. + Fixed bug where pressing return/enter in list items would sometimes move the caret the to top of the content area in FF. + Fixed bug where the style listbox wouldn't be updated correctly if you used the use_native_selects option. + Fixed bug where WebKit browsers would produce a div element when ending list elements using return. + Fixed so translation of popup contents only occurs if it's needed. + Optimized the URI object in regards or converting absolute URIs to relative URIs. +Version 3.1.1 (2008-08-18) + Added new getSize method to DOMUtils it will return the dimensions only of an element. + Added new alert/confirm methods to the tinyMCEPopup class to prevent focus problems and also to shorten method calls. + Added new plugin_preview_inline option to preview plugin to enable/disable native/inline dialogs. + Added new readonly option. If this is set the editor will only display the contents for the user. + Added missing tabindex and accesskey to input elements in the default valid_elements setup. + Updated firebug lite to 1.2, to enable it use the tiny_mce_dev.js?debug=1 on the development package. + Fixed so the preview dialog in the preview plugin uses inline dialogs/popups. + Fixed so CDATA sections remains intact through the serialization process of the DOM tree. + Fixed various issues with the getAttrib command. It will now return more correct values. + Fixed bug where the embed element wasn't properly parsed in the media plugin it now supports 3 formats. + Fixed bug where the noshade attribute was serialized incorrectly on IE. + Fixed bug where editing an existing link element didn't force it relative. + Fixed bug where image link creation fails on Safari if the image is aligned. + Fixed bug where it was possible to scroll the fullscreen mode in Opera 9.50. + Fixed bug where removal of center image alignment would fail. Patch contributed by Andrew Ozz. + Fixed bug where inlinedialogs didn't work properly if the doctype was incorrect in IE. + Fixed bug where cross domain loading didn't work correctly in Opera 9.50. + Fixed bug where breaking huge text blocks with return/enter key would scroll to end of block. + Fixed bug where replace button kept inserting the replacement text even if there is no more matches. + Fixed bug with fullpage plugin where value wasn't set correctly. Patch contributed by Pascal Chantelois. + Fixed bug where the dom utils setAttrib method call could produce an exception if the input was null/false. + Fixed bug where pressing backspace would sometimes remove one extra character in Gecko browsers. + Fixed bug where the native confirm/alert boxes would move focus to parent document if fired in dialogs. + Fixed bug where Opera 9.50 was telling you that the selection is collapsed even when it isn't. + Fixed bug where mceInsertContent would break up existing elements in Opera and Gecko. + Fixed bug where TinyMCE fails to detect some keyboard combos on Mac, contributed by MattyRob. + Fixed bug where replace all didn't move the caret to beginning of text before searching. + Fixed bug where the oninit callback wasn't executed correctly when the strict_loading_mode option was used, thanks goes to Nicholas Oxhoej. + Fixed bug where a access denied exception was thrown if some other script specified document.domain before loading TinyMCE. + Fixed so setting language to empty string will skip language loading if translations are made by some backend. + Fixed so dialog_type is automatically modal if you use the inlinepopups plugin use dialog_type : "window" to re-enable the old behavior. +Version 3.1.0.1 (2008-06-18) + Fixed bug where the Opera line break fix didn't work correctly on Mac OS X and Unix. + Fixed bug where IE was producing the default value the maxlength attribute of input elements. +Version 3.1.0 (2008-06-17) + Fixed bug where the paste as text didn't work correctly it encoded produced paragraphs and br elements. + Fixed bug where embed element in XHTML style didn't work correctly in the media plugin. + Fixed bug where style elements was forced empty in IE. The will now be wrapped in a comment just like script elements. + Fixed bug where some script elements wrapped in CDATA could fail to be serialized correctly. + Fixed bug where FF 3 produced -moz- internal styles in some style attributes. + Fixed bug where query strings and external URLs didn't work correctly in style attributes. + Fixed bug where shape attribute of area elements got serialized as rect regardless of it's initial value in IE 6. + Fixed bug where selection of elements inside layers would fail in IE since focus was moved to the document body. + Fixed bug where pressing enter/return in an editable select box would produce an __mce_add_custom__ class value. + Fixed bug where changing font size of text placed inside a colored text chunk would remove the parent node. + Fixed bug where Opera 9.5 final produced a strange line break behavior due to a workaround for previous Opera versions. + Fixed bug where text/background color would produce a strange focus problem when you tried to click on the body in IE. + Fixed issue where selecting the title of an listbox equals the old 2.x behavior of changing the value to an empty string. + Fixed issue where it was common for the media plugin to break if the _value attribute wasn't added for the param element. + Fixed issue where the wrong parent editor instance might be updated if you use fullscreen mode in an incorrect way. + Fixed issue where Safari was producing a warning about the base element not being closed correctly. + Removed redundant form element name matching from regexp in the DOMUtils class. +Version 3.0.9 (2008-06-02) + Added new contextmenu_offset_x/contextmenu_offset_y options for the contextmenu plugin. + Added cite attribute to the default rule for the blockquote element. + Added support for using arrow keys for selection of items in listboxes. + Added support for using arrow keys for selection of items in dropmenus. + Fixed bug where blockformat change on elements with BR inside them didn't change correctly on Firefox. + Fixed bug where removing table rows inside thead or tfoot would remove the whole table if it was the last one. + Fixed bug where XHR synchronous mode didn't execute the callback handlers synchronously. + Fixed bug where setting border to 0 didn't add border: 0 to the style attribute when using the advimage dialog. + Fixed bug where the selection of images and table cells didn't work correctly when the editor is placed in a frame and running on IE. + Fixed bug where the store/restore of a selection didn't work correctly in non IE browsers. + Fixed bug where only the first element would be invalid for the invalid_elements option. + Fixed bug where paste as plain text didn't encode the characters correctly when they where inserted. + Fixed bug where HTML source window couldn't be maximized on Gecko when the maximizable feature was enabled. + Fixed bug where color selection using the color picker could produce exception in IE. + Fixed bug where font size changes could produce produce extra redundant elements. + Fixed bug where IE could produce unknown runtime error if you replaced a image with another image from a separate frame. + Fixed bug where the domLoaded state for the Event class wasn't set correctly if the editor was loaded dynamically using the gzip compressor. + Fixed bug where handling of the base element for a page would produce incorrect urls. Based on a patch contributed by John LeSueur. + Fixed bug where table constraint alert boxes was presented with an empty value and wasn't the skinned inline ones. + Fixed bug where the onChange event wasn't fired when the form was submitted. It's now also triggered when the save method is called. + Fixed bug where encoding set to xml didn't work as expected. It now encodes the contents into XML entities. + Fixed bug where numrows didn't work correctly for the merge cells dialog of the table plugin. + Fixed bug where the onGetContent event was fired even when the no_events flag was set. + Fixed bug where the preview panels for the advimage and the media plugin could overflow on Safari and FF 3. + Fixed bug where the editing and removal of abbr elements using the xhtmlxtras plugin working correctly on IE. + Fixed bug where save button in the save plugin didn't work correctly on IE. + Fixed bug where dragging layers didn't work as expected since it would snap back to it's original location if you saved. + Fixed bug where the description of the template plugin dialog wasn't updated correctly. + Fixed bug where the values for frame and rules in the table dialogs where swapped. + Fixed bug where the elements like ins, del, cite, acronym and abbr didn't have the default editing style as the old 2.x branch. + Fixed bug where ask mode would lock the focused textarea if you pressed cancel in the confirm dialog on FF 3. + Fixed bug where ask mode would produce contents for empty textareas if you reloaded the page. + Fixed so the onGetContent event gets the full pass through object just like the other events. + Fixed so attributes for block elements remains the same when you change format of a element. +Version 3.0.8 (2008-04-30) + Fixed bug where IE would produce an error if textareas without names where converted. + Fixed bug where editor wasn't forced empty when there was only a single br or empty paragraph left. + Fixed bug where IE would produce an warning message if object elements where produced in the media plugins preview running on https. + Fixed bug where new addVer function didn't handle hash items correctly. Patch contributed by Mirek Burkon. + Fixed bug where font_size_style_values option wasn't applied correctly to fonts inside the editor. + Fixed bug where image selection could be lost if a image was edited using context menu on IE. + Fixed bug where style values wasn't updated properly due to an invalid regexp. + Fixed bug where IE 6 where displaying warning message about insecure items when inserting an image while using https. Patch contributed by Norifumi Sunaoka. + Fixed bug where IE was producing an auto save message if you selected a color from the color split button. + Fixed bug where backspace sometimes would move the caret to the end of the previous block in Gecko. + Fixed bug where the rowlayout manager didn't work as described in the documentation. + Fixed bug where the default options for the fullpage plugin wasn't applied correctly. + Fixed bug where selection would jump one character if you applied a styles to a words in non IE browsers. + Fixed bug where undo levels wasn't added correctly if you went back in undo history and added a new event. + Fixed bug where font size dropdown didn't mark the selected size in IE. + Fixed bug where the size of the editor was determined using clientWidth instead of offsetWidth. + Fixed so the onchange event doesn't fire on the initial undo level, it will also fire when the editor is blurred. + Fixed so the advhr plugin produces XHTML valid output instead of non standard attributes. + Fixed so blockquote gets converted into [quote] in when the bbcode plugin is enabled. + Fixed so theme_advanced_font_sizes can be named for example Font 1=1, Font 2=2 etc. + Fixed so editor_selector/editor_deselector can be regexps. By default only strings are allowed not part regexps like before. + Fixed so that the version suffix is optional. It still requires the build process so you need to export it manually. + Fixed so it's possible to tab to table cells in non Gecko browsers and also produce new rows if you tab at the end of a table. Contributed by Josh Peek. +Version 3.0.7 (2008-04-14) + Added new version suffix to all internal GET requests to make sure that the users cache gets cleared correctly. + Fixed issue with isDirty returning true event if it wasn't dirty on IE due to changes in tables during initialization. + Fixed memory leak in IE where if a page was unloaded before all images on the page was loaded it would leak. + Fixed bug in IE where underline and strikethrough could produce an exception error message. + Fixed bug where inserting paragraphs in totally empty table cells would produce odd effects. + Fixed bug where layer style data wasn't updated correctly due to some performance enhancements with the DOM serializer. + Fixed bug where it would convert the wrong element if there was two elements with the same name and id on the page. + Fixed bug where it was possible to add style information to the body element using the style plugin. + Fixed bug where Gecko would add an extra undo level some times due to the blur event. + Fixed bug where the underline icon would get active if the caret was inside a link element. + Fixed bug where merging th cells not working correctly. Patch contributed by André R. + Fixed bug where forecolorpicker and backcolorpicker buttons where rendered incorrectly when the o2k7 skin was used. + Fixed bug where comment couldn't contain -- since it's invalid markup. It will now at least not break on those invalid comments. + Fixed bug where apos wasn't handled correctly in IE. It will now convert apos to ' on IE since that browser doesn't support that entity. + Fixed bug where entities wasn't encoded correctly inside pre elements since they where protected from whitespace removal. + Fixed bug where color split buttons where rendered incorrectly on IE6 when using the non default theme. + Fixed so caret is placed after links ones they are created, to improve usability of the editor. + Fixed so you can select tables by clicking on it's borders in non IE browsers to normalize the behavior. + Fixed so the menus can be toggled by clicking once more on the icon in listboxes, menubuttons and splitbuttons based on code contributed by Josh Peek. + Fixed so buttons can be labeled, currently only works with the default skin, so it's kind of experimental. Patch contributed by Daniel Insley. + Fixed so forecolorpicker and backcolorpicker remembers the last selected color. Patch contributed by Shane Tomlinson. + Fixed so that you can only execute the mceAddEditor command once for the same instance name. + Fixed so command functions added with addCommand can pass though the call to default handles if it returns true. +Version 3.0.6.2 (2008-04-07) + Fixed bug where empty tables couldn't be edited correctly on non IE browsers if they where loaded into the editor. + Fixed bug where it was impossible to resize layers correctly in IE since it thought it was an image. + Fixed bug where an editor instance was stealing focus in IE resulting in a scroll to the editor on page reloads. + Fixed bug where Safari was crashing on Mac OS X if you closed dialogs using the Esc key. +Version 3.0.6.1 (2008-04-04) + Added support for the missing mceAddFrameControl command. The input for this command has changed so consult the Wiki. + Fixed bug where sub menus for the drop menus would leave an empty element behind. + Fixed memory leak in IE if the editor was placed in a frame or iframe. +Version 3.0.6 (2008-04-03) + Added elements to the default value of valid_elements option. It now contains all XHTML strict elements and a few transitional. + Added more accessibility fixes, it's now possible to navigate and close list boxes and split button menus with the keyboard. + Added missing getInfo method to the contextmenu and safari plugin, this caused problems for the Drupal module. + Added new inlinepopups_zindex option to the inlinepopups plugin so that you can configure the default start z-index. + Added new setControlType method to the tinymce.ControlManager class. This method enables you to override the default classes. + Added ability to specific an optional control class to use instead of the default one for the ControlManager methods. Based on concept by Josh Peek. + Fixed bug where attribute rules for the DOM Serializer couldn't contain - or _ characters in their names. + Fixed bug where inlinepopups event blocker and modal dialog blocker elements produced vertical scrollbars. + Fixed bug where there was a rendering issue with quirks mode in Safari moving the resize handle to an incorrect position. + Fixed bug with forecolor/backcolor controls on IE. Sometimes elements positioned relative will generate display errors. + Fixed bug where a p2 was leaking out in the global name space when you selected a color from the forecolor/backcolor controls. + Fixed bug where empty paragraphs didn't work as expected in browsers other than IE. + Fixed bug where the load method of the tinymce.dom.ScriptLoader didn't check if the file was already loaded. + Fixed bug where the load method for the PluginManager and ThemeManager didn't check if a plugin/theme by a specific name was all ready loaded. + Fixed bug where the theme_advanced_link_targets option didn't work correctly with the advanced themes link dialog. Patch contributed by Arnold B. + Fixed bug where the style command would merge classes into empty span elements. + Fixed bug where the style command would remove empty span elements outside the current selection. + Fixed bug where the fix for the Safari backspace bug removed all editor contents if it was filled with empty paragraphs. + Fixed bug where alert and confirm boxes opened by the inlinepopups plugin would produce an exception if domain relaxing was used. + Fixed bug where Safari was adding style attributes to all elements when you paste them into the editor. + Fixed bug where the spellchecker menus was visually incorrect since the space for the non existing icon was still there. + Fixed bug where remove_linebreaks option didn't remove line breaks inside the text contents of a element. + Fixed bug where Safari 3.1 was introducing _mc_tmp into paragraphs due to the new querySelectorAll and a TinyMCE specific workaround. + Fixed bug where getParam method in the Editor class was returning incorrect objects and would mess up the font drop down. Patch contributed by speednet. + Fixed bug where the table dialog would produce an exception in IE when you edited tables since it tried to place focus in a disabled field. + Fixed bug where class attribute on some span elements was removed on cleanup. + Fixed bug where resizing the editor in IE could produce an exception if the editor width/height got to be a negative value. + Fixed bug where wmv files wouldn't play since the src param was used instead of the url param. + Fixed bug where br elements would be added here and there in Gecko. Geckos internal _moz_dirty br elements where serialized as well. + Fixed bug where editing named anchors would produce two anchors instead of one updated one. + Fixed bug where arrow and function keys didn't work when an noneditable element was focused within the editor. + Fixed bug where the dispatcher could produce an exception if the listener list was altered inside an event callback. + Fixed bug where it was impossible to totally empty the editor contents on Safari due to an mistreatment of nbsp as whitespace. Patch contributed by Andrew Ozz. + Fixed bug where TinyMCE would not convert textareas with the same name attribute value. It will now generate an unique id for those textareas. + Fixed bug where backspace/delete key was deleting td elements inside tables while running on Gecko. + Fixed bug where Firefox 3.0b4 and Opera 9.26 where scrolling to the top of document when pressing return/enter. + Fixed bug where the template plugin wasn't just inserting the mceTmpl tagged element. + Fixed bug where the alert method of the default WindowManager implementation didn't translate input language strings like the inlinepopups dialog does. + Fixed bugs with the backspace behavior in Gecko. The caret was placed on incorrect locations in the DOM sometimes. + Fixed so advimage dialog and table dialogs has support for editable select boxes for the class value. + Fixed so the media, pagebreak and spellchecker doesn't load it's default content.css file if the content_css option is set to false. + Fixed so the paste_use_dialog option works again it's enabled by default but can be disabled on IE. Patch contributed by Speednet. + Fixed so that the fullscreen editor is focused when switching fullscreen editing on. + Fixed so it's possible to edit images and links inside tables using the context menu. + Fixed so table dialogs and the advanced image dialog doesn't loose selection in IE if the dialogs where navigated/submitted with the keyboard. + Fixed so the theme_advanced_blockformats options can have named items for example title 1=h1;title 2=h2. + Fixed so it's possible to add a custom editor_css for the simple theme. + Fixed quirks with directionality rtl, patch contributed by Andrew Ozz. + Fixed so the inlinepopups default start zIndex is 300000. + Fixed typo in media plugin Shockware is now replaced with Shockwave. + Fixed psuedo memory leak in IE with the replaceChild method inside the DOMUtils.replace method. + Fixed so memory is released when an editor instance is removed from page. + Optimized the color split button menus so that they use less event handlers. + Removed the util/mclayer.js file since it's no longer used by any of the TinyMCE dialogs and is considered deprecated. +Version 3.0.5 (2008-03-12) + Added new black skin variant to the o2k7 skin contributed by Stefan Moonen. + Added new explode method to the tinymce core class. This does a split but removed whitespace it also defaults to a , delimiter. + Added new detection logic for IE 8 standards mode into the DOMUtils class strMode can now be checked to see if that mode is on/off. + Added new noscale option value for the scale select box for Flash in the media plugin. + Fixed bug where the menu for the ColorSplitButton wasn't removed when the editor was removed. + Fixed bug where font colors couldn't be edited correctly since the style of the element didn't get updated correctly. + Fixed bug where class of elements would get lost when TinyMCE was fixing incorrect HTML markup. + Fixed bug where table editing would produce double height values. + Fixed bug where width style value wouldn't be removed if you switched width unit from cm/em to pixels or percent. + Fixed bug where the search/replace input box wasn't auto focused like the other dialogs. + Fixed bug where the old mceAddControl command would use the fullscreen settings next time it created an instance. + Fixed bug where multiple lines where added to the target cell if you merged multiple empty cells. + Fixed bug where drop down menus would be incorrectly positioned inside scrollable divs. + Fixed bug where the separators of the silver skin variant didn't display correctly in IE 6. + Fixed bug where createStyleSheet seems to load scripts at opposite order in some IE versions. + Fixed bug where directionality could produce odd results for the UI and the dialogs. + Fixed bug where the DOM serializer wouldn't serialize custom namespaced attributes in IE 6 using the *[*] valid elements rule. + Fixed bug where table caption would be inserted after the thead element if you swapped a tr to be inside the thead. + Fixed bug where the youtube detection logic for the media plugin was to generic. + Fixed so the deprecated and undocumented theme_advanced_path_location set to none won't hide the whole statusbar. + Fixed so most input lists can have whitespace in them they are now split using the new tinymce.explode method. + Fixed so the popup_css and popup_css_add URLs are relative to where the current document is located. + Fixed various bugs and quirks with the store/restore selection logic. + Fixed so the editor starts in IE 8 standards mode but still that browser is very very buggy. + Fixed so dialog_type set to modal will block the background and other inline windows and only give access to the front most window. +Version 3.0.4.1 (2008-03-08) + Fixed critical bug where it was impossible to edit images when inlinepopups where used due to lost selection in IE. +Version 3.0.4 (2008-03-07) + Added new option constrain_menus, this enables you to force view port constraints on all menus. Contributed by Shane Tomlinson. + Fixed bug where table background wasn't visible inside the editor due to a default CSS rule overriding the style attribute. + Fixed bug where links would get a null class added if no styles was used in IE. + Fixed bug where spellchecker was auto focusing the editor in IE. + Fixed bug where document.domain would produce invalid argument if the editor was loaded in IE6 over a network UNC path. + Fixed bug where table height attribute was used, this is deprecated in XHTML so it now adds it as an style. + Fixed bug where textareas with style values would produce error in IE. + Fixed so the first element in each dialog is focused by default to enhance keyboard usage. + Fixed so you can add a mceFocus class to elements to make it auto focused. + Fixed so you can close dialogs using the esc key. + Fixed so you can press return/enter to submit the action of each dialog. + Fixed so tabbing inside an inline popups wont focus the resize anchor elements. + Fixed so you can press ok in inline alert messages using the return/enter key. + Fixed so textareas can be set to non px or % sizes for example em, cm, pt etc. + Fixed so non pixel values can be used in width/height properties for tables. + Fixed so the custom context menu can be disabled by holding down ctrl key while clicking. + Fixed so the layout for the o2k7 skin looks better if you don't have separators before and after list boxes. + Fixed so the sub classes get a copy of the super class constructor function to ease up type checking. + Fixed so font sizes for the format block previews are normalized according to http://www.w3.org/TR/CSS21/sample.html (it can be overridden). + Fixed so font sizes for h1-h6 in the default content.css is normalized according to http://www.w3.org/TR/CSS21/sample.html (it can be overridden). +Version 3.0.3 (2008-03-03) + Fixed bug where an error about document.domain would be thrown if TinyMCE was loaded using a different port. + Fixed bug where mode exact would convert textareas without id or name if the elements option was omitted. + Fixed bug where the caret could be placed at an incorrect location when backspace was used in Gecko. + Fixed bug where local file:// URLs where converted into absolute domain URLs. + Fixed bug where an error was produced if a editor was removed inside an editor command. + Fixed bug where force_p_newlines didn't effect the paste plugin correctly. + Fixed bug where the paste plugin was producing an exception on IE if you pasted contents with middots. + Fixed bug where delete key could produce exceptions in Gecko sometimes due to the fix for the table cell bug. + Fixed bug where the layer plugin would produce an visual add class called mceVisualAid this one is now renamed to mceItemVisualAid to mark it internal. + Fixed bug where TinyMCE wouldn't initialize properly if ActiveX controls was disabled in IE. + Fixed bug where tables and other elements that had visual aids on them would produce an extra space after any custom class names. + Fixed bug where search with an empty string would produce some odd "invalid pointer" error in IE. + Fixed bug where elements like menus where placed at incorrect positions in Opera 9.26. + Fixed bug where IE was loosing focus of the editor when you clicked some dropmenu and if it was placed in a frame or iframe. + Fixed bug where focus of images could be lost in IE if you focused the accessibility confirm dialog in the advimage plugin. + Fixed bug where nestled font elements would produce odd output like missing font elements. + Fixed bug where text colors and styles got removed if invalid_elements included the font element. + Fixed bug where text-decoration set to underline or line-through would remove other styles from span elements. + Fixed bug where editor contents like \n\n would be incorrectly handled and processed as real line feeds. + Fixed bug where incorrectly encoded urls with ampersands in them would be decoded incorrectly. + Optimized the DOMUtils decode method to be a lot faster if the string doesn't have any entities to decode. +Version 3.0.2.1 (2008-02-26) + Fixed alert/confirm dialogs so they display correctly. +Version 3.0.2 (2008-02-26) + Added new body_id option that enables you to specify the id of the body inside the editor iframe based on ideas by David Bildström (ChronoZ). + Added new body_class option that enables you to set the class for the body of the editor iframe based on ideas by David Bildström (ChronoZ). + Added new CSS class to the default content.css files mceForceColors that forces white background and black text can be used with the body_class option. + Added new type parameter to the Editor.getParam function to reduce redundant logic for parsing hash tables. + Added new isDone method to the ScriptLoaded class, this enables you to check if a script has been loaded or not. + Added new resizeTo and resizeBy methods for the advanced theme. Can be called using tinyMCE.activeEditor.theme.resizeTo(w, h); + Added new skin_variant option this can be used to extend existing skins with slight modifications like color. + Added new variant of the o2k7 skin called "silver" based on a contribution made by Stefan Moonen. + Fixed bug where the template plugin might produce errors if the template_mdate_classes wasn't configured. + Fixed bug where the media plugin didn't convert the URLs for movies once they where inserted. + Fixed bug where the style field for the advlink dialog didn't work correctly if you edited an existing link. + Fixed bug where alignment of toolbars would fail in editor was uses in a quirks mode on IE, fix contributed by Peter Wood & Art Lawry. + Fixed bug where initialization of multiple editors at the same time using the mceAddControl method would produce errors. + Fixed bug where initialization of editors using mceAddControl command or new tinymce.Editor calls would fail during page load. + Fixed bug where the check for domain relaxing could fail if the document.domain property was changed by another script. + Fixed bug where textareas couldn't be named description or any other name that matches the meta elements in IE and Opera. + Fixed bug where the element path would fail sometimes in IE due to "unknown runtime error" on innerHTML. + Fixed bug where Safari would crash if you was hiding the editor before serializing the contents. + Fixed bug where the editor wasn't scaled propertly in fullscreen mode using the old fullscreen_new_window option. + Fixed bug where render method didn't load language packs in IE and Opera if you rendered an editor during page load. + Fixed bug where resizing the browser window in fullscreen didn't resize the editor. + Fixed bug where the blockquote command didn't move the caret inside the new empty blockquote if you used it on an empty document. + Fixed bug where auto in a style width/height for the textarea would produce an editor with the size value of 100. Fix contributed by Shane Tomlinson. + Fixed bug where restoration of selection at the beginning of an element could fail in Gecko. + Fixed bug where caret restoration after a cleanup could place the it at an incorrect location. + Fixed bug where delete key inside td elements would delete the cell in Gecko. + Fixed so the blockquote button toggles individual lines. This behavior is a bit more like the old indentation behavior in the 2.x branch. + Fixed so the dialog language packs only gets loaded the first time you open a dialog. + Fixed so all classes in the whole UI is prefixed with "mce" to avoid collisions, use the skin converter to update your existing skins. + Fixed so all classes in the inlinepopups logic is prefixed with "mce" to avoid collisions, use the skin converter to update your existing skins. + Fixed so that the window in fullscreen mode can be resized when fullscreen_new_window option is enabled. + Fixed so blockquote elements are formatted in the source output with an linefeed before and after it. + Optimized the editor initialization by reducing the number of calls to getBookmark/moveToBookmark. +Version 3.0.1 (2008-02-21) + Added spellchecker plugin into the main package, but without any backend can be specified with the spellchecker_rpc_url option. + Added src attribute for script elements to the default valid_elements option value. + Added extra parameter to the class_filter callback it can now also filter out classes based on the whole CSS rule. + Added support for domain relaxing, TinyMCE can now be loaded from an remote domain as long as they are on the same root domain. + Added support for custom elements the new custom_elements option enables you to add non HTML elements to the editor. + Added support for the W3C Selectors API that was added to latest nightly build of WebKit. + Fixed bug where some object param element wasn't stored correctly using the media plugin. + Fixed bug where Opera was scrolling to top of page is drop menus on list boxes where displayed. + Fixed bug where IE6 was crashing if a format block was used on a container with anchor elements. + Fixed bug where spans with font sizes wasn't handled correctly when editor was loading contents. + Fixed bug where mode exact couldn't convert editors with name only. Id is no longer required but recommended. + Fixed bug where the mceInsertRawHTML command produced an extra undo level. + Fixed bug where the specific_textareas mode didn't work correctly this is the same thing as textareas now. + Fixed bug where the values of input elements in the HTML page of dialogs pages where changed in IE. + Fixed bug where fullscreen and fullpage plugins didn't work well together. + Fixed bug where embed elements wasn't handled properly in the media plugin. + Fixed bug where style information on span elements gets munged when fonts are converted to spans. + Fixed bug where some entities in element attributes where encoded incorrectly in the latest WebKit build. + Fixed bug where initialization would fail in IE if there where two input elements with the name submit in the form. + Fixed bug where fullscreen mode didn't work correctly in IE when the fullscreen_new_window option was used. + Fixed bug where invalid contents like an ul inside a p element would produce odd results in IE. + Fixed bug where Opera 9.2x was placing the drop menus at incorrect locations if the editor was placed in a table. + Fixed bug where Opera was producing odd results if enter/return was pressed while having forced_root_blocks disabled. + Fixed bug where layer plugin was stealing focus in IE on initialization. + Fixed bug where body attributes wasn't set properly in the fullpage plugin, fix contributed by Hiroaki Kawai. + Fixed bug where insert image and insert link dialogs where producing an extra level in the undo history. + Fixed bug where Gecko would produce an error if empty elements like
    where inserted using mceInsertContent. + Fixed bug where center alignment of images produced odd results inside table cells. + Fixed bug where center alignment of images couldn't be toggled correctly. + Fixed bug where alignment of images inside tables would produce double float style items in IE if the fix_table_elements option was enabled. + Fixed bug where a variable called 'v' was polluting the global namespace. Objects tinymce and tinyMCE are the only ones allowed to be global. + Fixed bug where insert table from context menu couldn't insert new tables inside existing tables. + Fixed bug where Safari wouldn't produce br elements on enter when the force_br_newlines option was enabled. + Fixed bug where switching cell type in table cell dialog would produce odd attributes in IE. + Fixed bug where Gecko was outputting internal attributes if valid_elements where set to "*[*]". + Fixed bug where the style plugin would produce non hex colors inside the dialog when running on Gecko. + Fixed bug where an empty src value for insert image would remove the currently selected image if it wasn't and image element. + Fixed bug where hidden input elements would break the logic for the tab_focus option. + Fixed bug where save button wasn't working correctly in fullscreen mode. + Fixed bug where the editor was forced to be placed in a form element if the save_onsavecallback option was used. + Fixed bug where upper case param attributes wasn't parsed correctly in the media plugin. + Fixed bug where render method of tinymce.Editor class would produce an exception if the strict_loading_mode option was omitted. + Fixed bug where nodeChanged event could be fired while the editor was loading and there for produce an exception in FF. + Fixed bug where no undo levels where added if the user created new table rows using the tab key on Gecko. + Fixed bug where tables would be broken if you selected a different block format for contents withing an table cell. + Fixed bug where the render method of the tinymce.Editor class didn't setup the tinymce.EditorManager.settings object correctly. + Fixed bug where the advanced image dialog would go to the first tab if the alternative image was changed using the file browser link. + Fixed bug where the forced_root_block option would produce BR elements inside empty blocks if the block wasn't a paragraph. + Fixed bug where the forced_root_block doesn't work correctly on IE if the specified element was something else than paragraphs. + Fixed bug where selection of images would get lost if user selected something from the context menu in IE. + Fixed bug where the context menu plugin would pollute the global namespace with two variables p1 and p2. + Fixed compatibility issue with Mootools, it is destroying document.getElementById on unload in IE. (Mantra: You don't own the internal objects). + Fixed bugs where dialogs/tabs and other UI elements where rendered incorrectly in Firefox 3. + Fixed so the auto CSS class importer is compatible with 2.x. + Fixed so the editor UI and inlinedialogs works correctly with the YUI CSS reset package. + Fixed so header and footer elements are forced to lower case when the fullpage plugin is used. + Fixed so load prefixes "-" for plugins and themes isn't required if the plugin/theme was loaded by the ThemeManager/PluginManager. + Fixed so the JSONRequest uses application/json content type to make Ruby on rails happy. + Fixed so the CSS rule is more exact for the body in the default content.css files. Body is now defined as "body.mceContentBody" instead of just "body". + Fixed so the tiny_mce_dev.js uses XHR instead of document.write to load scripts to resolve an issue with Opera 9.50. + Fixed so language pack loading can be disabled by setting the language option to false. Can be useful for systems with their own language pack management. +Version 3.0 (2008-01-30) + Added map and area elements to the default valid_elements list and also some indentation rules. + Fixed bug where empty paragraphs wasn't padded when loading contents. + Fixed bug where the RowLayout manager didn't work at all. + Fixed bug where style attribute data would get messed up in advimage dialog. + Fixed bug where the table dialogs class select wasn't updated correctly. + Fixed bug where elements would get extra whitespace around on insert when body was present in valid_elements. + Fixed bug where coords attribute of the area element wasn't handled properly in IE. + Fixed bug where Safari didn't produce BR elements on shift+return. + Fixed bug where force blocks would cast odd invalid attribute exception in IE. + Fixed bug where media plugin would produce extra whitespace before and after objects. + Fixed bug where cleanup_callback could break the contents of the editor. But use the new event system instead of this option. + Fixed bug where the tab_focus option didn't work between editor instanced. You can now tab between editors. + Fixed bug where the load function of the ScriptLoader class didn't load single files without the load que as it was supposed to. + Fixed bug where the execcommand_callback parameter order was incorrect. Recommendation use the new addCommand method. + Fixed bug where range.select calls sometimes failed on some IE versions. + Fixed bug where Safari was scrolling to top of document when enter/returned was pressed. + Fixed bug where fullscreen_new_window option didn't work correctly. + Fixed bug where the nonbreaking plugin inserted an space instead of an non breaking space the first time. + Fixed bug where the visualization of non breaking spaces where visual in element path. + Fixed so the focus is restored to the editor after inserting an custom character. + Fixed so the isNotDirty state is set to false if a new undo level is added. + Fixed so pointless style information for borders gets removed in IE. + Fixed so the resize button has a se-resize cursor css value. +Version 3.0rc2 (2008-01-18) + Added new fix_nesting option to fix bug #1867292, this is disabled by default. + Added new indentation option enables you to specify how much each indent/outdent call will add/remove. + Added easier support for enabling/disabling icon columns on drop menues. + Added new menu button control class. This control is very similar to the splitbutton but without any onclick action. + Added support for previous tab focus (shift+tab). The tab_focus setting now takes two items next and previous element. + Fixed bug where iframes inside the editor got removed in Firefox on initial load. + Fixed bug where the CSS for abbr elements wasn't applied correctly in IE. + Fixed bug where mceAddControl on element inside a hidden container produced errors. + Fixed bug where closed anchors like produced strange results. + Fixed bug where caret would jump to the top of the editor if enter was pressed a the end of a list. + Fixed bug where remove editor failed if the editor wasn't properly initialized. + Fixed bug where render call on for a non existing element produced exception. + Fixed bug where parent window was hidden when the color picker was used in a non inlinepopups setup. + Fixed bug where onchange event wasn't fired correctly on IE when color picker was used in dialogs. + Fixed bug where save plugin could not save contents if the converted element wasn't an textarea. + Fixed bug where events might be fired even after an editor instance was removed such as blur events. + Fixed bug where an exception about undefined undo levels could be throwed sometimes. + Fixed bug where the plugin_preview_pageurl option didn't work. + Fixed bug where adding/removing an editor instance very fast could produce problems. + Fixed bug where the link button was highlighted when an anchor element was selected. + Fixed bug where the selected contents where removed if a new anchor element was added. + Fixed bug where splitbuttons where rendered one pixel down in the default theme. + Fixed bug where some buttons where placed at incorrect positions in the o2k7 theme. + Fixed bug that made it impossible to visually disable a custom button that used an image instead of CSS sprites. + Fixed bug where it wasn't possible to press delete/backspace if the editor was added+removed and re-added due to a FF bug. + Fixed bug where an entities option with only 38,amp,60,lt,62,gt would fail in IE. + Fixed bug where innerHTML sometimes generated unknown runtime error on IE. + Fixed bug where content_css files wasn't loaded in the template preview iframe. + Fixed bug where scroll position was incorrect when toggling fullscreen mode. + Fixed bug where restoration of overflow didn't work correctly when disabling fullscreen mode in Opera. + Fixed bug where drop menus where places at incorrect locations if the editor was placed in a scrollable container element. + Fixed bug where hideMenu didn't hide sub menus correctly. It will now hide all menus recursively. + Fixed so theme_advanced_path_location can be used in init options for compatibility reasons. + Fixed so the drop menu colors matches the rest of o2k7 theme. + Fixed so the preview example.html file is updated to the new 3.x API. + Fixed so the margins are the same by default inside the editable area between IE and other browsers. + Fixed so editor contents gets stored before it the onSubmit event is fired. +Version 3.0rc1 (2008-01-08) + Added new classes for toolbar rows in advanced theme mceToolbarRow1..n enabled you to change appearance of individual rows. + Added auto detection for the strict_loading_mode option when running in application/xhtml+xml mode on Gecko. + Optimized the HTML serializer by bundling some post process methods together. + Fixed so that the toolbars have unique IDs, enables you to alter the toolbars using the ControlManager and the DOM. + Fixed bug where delta values for dialog sizes in language packs didn't work correctly due to missing string to number casting. + Fixed bug where paragraph generation logic didn't handle hr or table elements correctly if they where the only element. + Fixed bug where some elements got extra linebreaks added after or before it in HTML output. + Fixed bug where it was hard to modify existing style data on table rows and table cells. + Fixed bug where the dom.getRect method didn't handle non pixel values correctly. + Fixed bug where strikethrough and underline couldn't be toggled on existing span elements. + Fixed bug where the postprocessor searched for nsbp instead of nbsp entities. + Fixed bug where it was impossible to edit links that had child elements within them. + Fixed bug where it was possible to click on the parent item of a submenu. + Fixed bug where mouseover/mouseout images couldn't be removed in advimage dialog. + Fixed bug where drop menus didn't work when running in application/xhtml+xml mode. + Fixed bug where Opera added doctype to output in application/xhtml+xml mode. + Fixed bug where some DOM methods didn't work correctly in the application/xhtml+xml mode. + Fixed bug where the inlinepopups didn't work correctly in the application/xhtml+xml mode. + Fixed bug where the ColorSplitButton didn't display correctly in the application/xhtml+xml mode. + Fixed bug where the UI layout was incorrect on Gecko browsers when running in application/xhtml+xml mode. + Fixed bug where the word paste plugin produced exception while running in application/xhtml+xml mode. + Fixed bug where there wasn't any hidden input element generated for divs while running in application/xhtml+xml mode. + Fixed bug where indentation of script/style/pre elements where incorrect. + Fixed bug where script element contents was removed in IE. + Fixed bug where script element contents got entity encoded. + Fixed bug where you couldn't edit existing element styles using the styles plugin. + Fixed bug where styles wasn't updated properly sometimes due to an performance enhancement. + Fixed bug where font sizes couldn't be changed using the style plugin. + Fixed bug where an error was produced in Gecko browsers when switching back from fullscreen mode. + Fixed bug where Opera was producing br elements after elements like h3. + Fixed bug where TinyMCE couldn't be loaded on a page using - characters in it's URL. + Fixed bug where the editor container element was forced to have a specific name. + Fixed bug with force_br_newlines option on Firefox, even though it should never be used (Read FAQ). + Fixed bug where onclick event had an return true; prefix added when creating an popup. + Fixed bug where the theme_advanced_statusbar_location option couldn't handle the value "none". + Fixed issue with URLs with multiple at characters for example an Zope URI. + Fixed so simple and advanced themes doesn't collide. + Fixed so a elements gets removed when the href field is left empty, the href attribute is required in a link after all. + Fixed so img elements gets removed when the src field is left empty, the src attribute is required for all images after all. + Removed the indent and encode methods from the tinymce.dom.Serializer class due to performance enhancement and reduction of the API size. +Version 3.0b3 (2007-12-14) + Added new getElement method to Editor class, returns the element that was replaced with the editor instance. + Added new unavailable prefix for disabled controls for accessibility reasons. + Fixed bug where regexp patterns couldn't be used for the editor_selector/editor_deselector options. + Fixed bug where the DOM wasn't properly initialized before the onInit event was executed in popups. + Fixed bug where font sizes where reduced by font size actions on previous spans in Safari. + Fixed bug where HR elements got places at the wrong location in IE. + Fixed bug where align/justify didn't work correctly on multiple paragraphs. + Fixed bug with missing translation for cell scope settings. + Fixed bug where selection/caret position was lost on some table actions. + Fixed bug where editor instances couldn't be added to hidden div elements. + Fixed bug where list elements in Safari would get an odd ID attribute. + Fixed bug where IE would return when the editor was completely empty. + Fixed bug where accessibility title attribute for access keys wasn't setup properly. + Fixed bug where forecolorpicker and backcolorpicker control names wasn't working. + Fixed bug where inserting template content didn't work in Safari due to selection exception. + Fixed bug where absolute URLs to remote hosts couldn't be used for background images. + Fixed bug where mysterious span elements where produced in Safari when injecting HTML contents. + Fixed bug where the media plugin didn't work correctly on the latest Opera 9.24. + Fixed bug where indentation of HTML output wasn't applied to all block elements. + Fixed bug where Safari was production DOM exception if you pressed enter in an empty editor. + Fixed bug where media plugin didn't parse script tags correctly patch contributed by Mathieu Campagna. + Fixed bug where the drop menus of list boxes like blockformat could produce scrolling of the page. + Fixed bug where the drop menus where placed at an incorrect location if TinyMCE was placed in a scrollable div. + Fixed bug where submit buttons couldn't be named submit, it's not recommended to name submit buttons submit anyway. + Fixed bug where the stylelistbox produced an exception if there was only one class in the list box. + Fixed bug where the stylelistbox wasn't updated correctly when the current class was removed. + Fixed bug where the formatblock command sometimes removed the body element. + Fixed bug where fullscreen switching in IE sometimes produced an exception when the spellchecker plugin was enabled. + Fixed issue where FF produced an empty paragraph when the editor was completely empty. + Fixed issue with size of image dialog in the advanced theme. + Fixed issues with the bbcode plugin it now also handles spans and the [font] rule. + Fixed so the style compression feature is a bit smarter to resolve issues with Opera. + Reintroduced the remove_linebreaks option, this is enabled by default. +Version 3.0b2 (2007-11-29) + Added type and compact attributes to the default valid_elements list for the ul and ol elements. + Added missing accessibility support to native list boxes in both the toolbar and dialogs. + Added missing access key for the element path for accessibility reasons. + Fixed support for loading themes from external URLs. + Fixed bug where setOuterHTML didn't work correctly when multiple elements where passed to it. + Fixed bug with visualchars plugin was moving elements around in the DOM. + Fixed bug with DIV elements that got converted into editors on IE. + Fixed bug with paste plugin using the old event API. + Fixed bug where the spellchecker was removing the word when it was ignored. + Fixed bug where fullscreen wasn't working properly. + Fixed bug where the base href element and attribute was ignored. + Fixed bug where redo function didn't work in IE. + Fixed bug where content_css didn't work as previous 2.x branch. + Fixed bug where preview dialog was throwing errors if the content_css wasn't defined. + Fixed bug where the theme_advanced_path option didn't work like the 2.x branch. + Fixed bug where the theme_advanced_statusbar_location was called theme_advanced_status_location. + Fixed bug where the strict_loading_mode o