From 650d4f06752fcebbd58945eb31c9485b1e580864 Mon Sep 17 00:00:00 2001 From: Charlie DeTar Date: Fri, 9 Jan 2015 18:14:36 -0700 Subject: [PATCH] WIP: Note editing, markdown to html --- karmaworld/apps/notes/forms.py | 12 +- .../0019_auto__add_field_notemarkdown_html.py | 159 + .../notes/migrations/0020_markdown_to_html.py | 161 + karmaworld/apps/notes/models.py | 17 + karmaworld/apps/notes/tests.py | 43 +- karmaworld/apps/notes/views.py | 25 +- karmaworld/apps/wysihtml5/__init__.py | 0 karmaworld/apps/wysihtml5/models.py | 0 .../apps/wysihtml5/static/wysihtml5/init.js | 17 + .../apps/wysihtml5/static/wysihtml5/test.html | 20 + .../wysihtml5/static/wysihtml5/toolbar.css | 0 .../wysihtml5/wysihtml-0.4.17/.gitignore | 7 + .../wysihtml-0.4.17/CHANGELOG.textile | 132 + .../wysihtml5/wysihtml-0.4.17/Gruntfile.js | 155 + .../static/wysihtml5/wysihtml-0.4.17/LICENSE | 9 + .../wysihtml5/wysihtml-0.4.17/README.markdown | 66 + .../wysihtml5/wysihtml-0.4.17/bower.json | 28 + .../dist/wysihtml5x-toolbar.js | 14508 ++++++++++++++++ .../dist/wysihtml5x-toolbar.min.js | 9 + .../dist/wysihtml5x-toolbar.min.map | 1 + .../wysihtml-0.4.17/dist/wysihtml5x.js | 13800 +++++++++++++++ .../wysihtml-0.4.17/dist/wysihtml5x.min.js | 9 + .../wysihtml-0.4.17/dist/wysihtml5x.min.map | 1 + .../wysihtml-0.4.17/examples/advanced.html | 241 + .../examples/advanced_div.html | 473 + .../examples/css/stylesheet.css | 133 + .../wysihtml-0.4.17/examples/jquery.1.10.2.js | 6 + .../wysihtml-0.4.17/examples/simple.html | 125 + .../wysihtml-0.4.17/examples/wotoolbar.html | 515 + .../wysihtml-0.4.17/lib/base/base.js | 139 + .../wysihtml5/wysihtml-0.4.17/package.json | 36 + .../wysihtml-0.4.17/parser_rules/advanced.js | 561 + .../parser_rules/advanced_and_extended.js | 683 + .../parser_rules/advanced_unwrap.js | 666 + .../wysihtml-0.4.17/parser_rules/simple.js | 32 + .../wysihtml-0.4.17/src/assert/html_equal.js | 103 + .../wysihtml5/wysihtml-0.4.17/src/browser.js | 387 + .../wysihtml5/wysihtml-0.4.17/src/commands.js | 102 + .../src/commands/addTableCells.js | 21 + .../src/commands/alignCenterStyle.js | 14 + .../src/commands/alignLeftStyle.js | 14 + .../src/commands/alignRightStyle.js | 14 + .../src/commands/bgColorStyle.js | 43 + .../wysihtml-0.4.17/src/commands/bold.js | 15 + .../src/commands/createLink.js | 102 + .../src/commands/createTable.js | 29 + .../src/commands/deleteTableCells.js | 40 + .../wysihtml-0.4.17/src/commands/fontSize.js | 18 + .../src/commands/fontSizeStyle.js | 34 + .../wysihtml-0.4.17/src/commands/foreColor.js | 18 + .../src/commands/foreColorStyle.js | 48 + .../src/commands/formatBlock.js | 223 + .../src/commands/formatCode.js | 50 + .../src/commands/formatInline.js | 150 + .../src/commands/indentList.js | 41 + .../src/commands/insertBlockQuote.js | 38 + .../src/commands/insertHTML.js | 13 + .../src/commands/insertImage.js | 98 + .../src/commands/insertLineBreak.js | 20 + .../src/commands/insertList.js | 159 + .../src/commands/insertOrderedList.js | 9 + .../src/commands/insertUnorderedList.js | 9 + .../wysihtml-0.4.17/src/commands/italic.js | 14 + .../src/commands/justifyCenter.js | 14 + .../src/commands/justifyFull.js | 14 + .../src/commands/justifyLeft.js | 14 + .../src/commands/justifyRight.js | 14 + .../src/commands/mergeTableCells.js | 30 + .../src/commands/outdentList.js | 78 + .../wysihtml-0.4.17/src/commands/redo.js | 9 + .../src/commands/removeLink.js | 48 + .../wysihtml-0.4.17/src/commands/underline.js | 9 + .../wysihtml-0.4.17/src/commands/undo.js | 9 + .../wysihtml-0.4.17/src/dom/auto_link.js | 149 + .../wysihtml-0.4.17/src/dom/class.js | 33 + .../src/dom/compare_document_position.js | 64 + .../wysihtml-0.4.17/src/dom/contains.js | 16 + .../src/dom/contenteditable_area.js | 69 + .../src/dom/convert_to_list.js | 106 + .../src/dom/copy_attributes.js | 35 + .../wysihtml-0.4.17/src/dom/copy_styles.js | 73 + .../wysihtml-0.4.17/src/dom/delegate.js | 26 + .../wysihtml-0.4.17/src/dom/dom_node.js | 81 + .../wysihtml-0.4.17/src/dom/get_as_dom.js | 64 + .../wysihtml-0.4.17/src/dom/get_attribute.js | 31 + .../wysihtml-0.4.17/src/dom/get_attributes.js | 33 + .../src/dom/get_parent_element.js | 72 + .../src/dom/get_pasted_html.js | 41 + .../wysihtml-0.4.17/src/dom/get_style.js | 73 + .../wysihtml-0.4.17/src/dom/get_textnodes.js | 13 + .../src/dom/has_element_with_class_name.js | 34 + .../src/dom/has_element_with_tag_name.js | 28 + .../wysihtml-0.4.17/src/dom/insert.js | 15 + .../wysihtml-0.4.17/src/dom/insert_css.js | 27 + .../src/dom/is_loaded_image.js | 14 + .../wysihtml-0.4.17/src/dom/line_breaks.js | 62 + .../wysihtml-0.4.17/src/dom/observe.js | 51 + .../wysihtml-0.4.17/src/dom/parse.js | 847 + .../wysihtml-0.4.17/src/dom/query.js | 18 + .../src/dom/remove_empty_text_nodes.js | 19 + .../wysihtml-0.4.17/src/dom/rename_element.js | 35 + .../src/dom/replace_with_child_nodes.js | 30 + .../wysihtml-0.4.17/src/dom/resolve_list.js | 93 + .../wysihtml-0.4.17/src/dom/sandbox.js | 252 + .../wysihtml-0.4.17/src/dom/set_attributes.js | 14 + .../wysihtml-0.4.17/src/dom/set_styles.js | 19 + .../src/dom/simulate_placeholder.js | 52 + .../wysihtml-0.4.17/src/dom/table.js | 878 + .../wysihtml-0.4.17/src/dom/text_content.js | 29 + .../wysihtml-0.4.17/src/dom/unwrap.js | 8 + .../wysihtml5/wysihtml-0.4.17/src/editor.js | 242 + .../wysihtml-0.4.17/src/lang/array.js | 126 + .../wysihtml-0.4.17/src/lang/dispatcher.js | 50 + .../wysihtml-0.4.17/src/lang/object.js | 68 + .../wysihtml-0.4.17/src/lang/string.js | 66 + .../wysihtml-0.4.17/src/polyfills.js | 130 + .../src/quirks/clean_pasted_html.js | 77 + .../src/quirks/ensure_proper_clearing.js | 23 + .../src/quirks/get_correct_inner_html.js | 30 + .../wysihtml-0.4.17/src/quirks/redraw.js | 23 + .../src/quirks/style_parser.js | 85 + .../src/quirks/table_cells_selection.js | 117 + .../src/selection/html_applier.js | 644 + .../src/selection/selection.js | 1006 ++ .../wysihtml-0.4.17/src/toolbar/dialog.js | 200 + .../src/toolbar/dialog_bgColorStyle.js | 58 + .../src/toolbar/dialog_createTable.js | 9 + .../src/toolbar/dialog_fontSizeStyle.js | 26 + .../src/toolbar/dialog_foreColorStyle.js | 58 + .../wysihtml-0.4.17/src/toolbar/speech.js | 90 + .../wysihtml-0.4.17/src/toolbar/toolbar.js | 325 + .../wysihtml-0.4.17/src/undo_manager.js | 215 + .../wysihtml-0.4.17/src/views/composer.js | 458 + .../src/views/composer.observe.js | 386 + .../src/views/composer.style.js | 201 + .../wysihtml-0.4.17/src/views/synchronizer.js | 97 + .../wysihtml-0.4.17/src/views/textarea.js | 71 + .../wysihtml-0.4.17/src/views/view.js | 54 + .../wysihtml-0.4.17/src/wysihtml5.js | 38 + .../wysihtml-0.4.17/test/browser_test.js | 85 + .../test/dom/auto_link_test.js | 111 + .../dom/compare_document_position_test.js | 21 + .../wysihtml-0.4.17/test/dom/contains_test.js | 18 + .../test/dom/convert_to_list_test.js | 101 + .../test/dom/copy_attributes_test.js | 51 + .../test/dom/copy_styles_test.js | 110 + .../wysihtml-0.4.17/test/dom/delegate_test.js | 62 + .../wysihtml-0.4.17/test/dom/dom_node_test.js | 116 + .../test/dom/get_as_dom_test.js | 55 + .../test/dom/get_parent_element_test.js | 190 + .../test/dom/get_style_test.js | 54 + .../dom/has_element_with_class_name_test.js | 29 + .../dom/has_element_with_tag_name_test.js | 25 + .../test/dom/insert_css_test.js | 51 + .../wysihtml-0.4.17/test/dom/observe_test.js | 83 + .../wysihtml-0.4.17/test/dom/parse_test.js | 903 + .../test/dom/rename_element_test.js | 28 + .../test/dom/resolve_list_test.js | 78 + .../wysihtml-0.4.17/test/dom/sandbox_test.js | 184 + .../test/dom/set_attributes_test.js | 15 + .../test/dom/set_styles_test.js | 19 + .../wysihtml-0.4.17/test/dom/table_test.js | 258 + .../wysihtml-0.4.17/test/dom/unwrap_test.js | 14 + .../test/editor_commands_test.js | 382 + .../test/editor_contenteditablemode_test.js | 410 + .../wysihtml-0.4.17/test/editor_test.js | 565 + .../wysihtml-0.4.17/test/incompatible_test.js | 62 + .../wysihtml5/wysihtml-0.4.17/test/index.html | 171 + .../wysihtml-0.4.17/test/lang/array_test.js | 22 + .../wysihtml-0.4.17/test/lang/object_test.js | 46 + .../wysihtml-0.4.17/test/lang/string_test.js | 23 + .../test/quirks/clean_pasted_html_test.js | 152 + .../wysihtml-0.4.17/test/undo_manager_test.js | 94 + .../wysihtml5/templates/wysihtml5/widget.html | 16 + .../apps/wysihtml5/templatetags/__init__.py | 0 .../apps/wysihtml5/templatetags/wysihtml5.py | 29 + karmaworld/apps/wysihtml5/widgets.py | 22 + karmaworld/assets/js/note-detail.js | 14 +- karmaworld/settings/common.py | 1 + karmaworld/templates/notes/note_base.html | 22 + karmaworld/templates/notes/note_detail.html | 6 +- requirements.txt | 3 + 182 files changed, 48008 insertions(+), 24 deletions(-) create mode 100644 karmaworld/apps/notes/migrations/0019_auto__add_field_notemarkdown_html.py create mode 100644 karmaworld/apps/notes/migrations/0020_markdown_to_html.py create mode 100644 karmaworld/apps/wysihtml5/__init__.py create mode 100644 karmaworld/apps/wysihtml5/models.py create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/init.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/test.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/toolbar.css create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/.gitignore create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/CHANGELOG.textile create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/Gruntfile.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/LICENSE create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/README.markdown create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/bower.json create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.min.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.min.map create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x.min.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x.min.map create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/advanced.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/advanced_div.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/css/stylesheet.css create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/jquery.1.10.2.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/simple.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/examples/wotoolbar.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/lib/base/base.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/package.json create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_and_extended.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_unwrap.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/simple.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/assert/html_equal.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/browser.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/addTableCells.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/alignCenterStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/alignLeftStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/alignRightStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/bgColorStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/bold.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/createLink.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/createTable.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/deleteTableCells.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/fontSize.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/fontSizeStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/foreColor.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/foreColorStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/formatBlock.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/formatCode.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/formatInline.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/indentList.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertBlockQuote.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertHTML.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertImage.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertLineBreak.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertList.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertOrderedList.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/insertUnorderedList.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/italic.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/justifyCenter.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/justifyFull.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/justifyLeft.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/justifyRight.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/mergeTableCells.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/outdentList.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/redo.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/removeLink.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/underline.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/commands/undo.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/auto_link.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/class.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/compare_document_position.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/contains.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/contenteditable_area.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/convert_to_list.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/copy_attributes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/copy_styles.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/delegate.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/dom_node.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_as_dom.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_attribute.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_attributes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_parent_element.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_pasted_html.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_style.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/get_textnodes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/has_element_with_class_name.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/has_element_with_tag_name.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/insert.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/insert_css.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/is_loaded_image.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/line_breaks.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/observe.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/parse.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/query.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/remove_empty_text_nodes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/rename_element.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/replace_with_child_nodes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/resolve_list.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/sandbox.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/set_attributes.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/set_styles.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/simulate_placeholder.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/table.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/text_content.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/dom/unwrap.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/editor.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/lang/array.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/lang/dispatcher.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/lang/object.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/lang/string.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/polyfills.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/clean_pasted_html.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/ensure_proper_clearing.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/get_correct_inner_html.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/redraw.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/style_parser.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/quirks/table_cells_selection.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/selection/html_applier.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/selection/selection.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/dialog.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/dialog_bgColorStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/dialog_createTable.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/dialog_fontSizeStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/dialog_foreColorStyle.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/speech.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/toolbar/toolbar.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/undo_manager.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/composer.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/composer.observe.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/composer.style.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/synchronizer.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/textarea.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/views/view.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/wysihtml5.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/browser_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/auto_link_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/compare_document_position_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/contains_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/convert_to_list_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/copy_attributes_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/copy_styles_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/delegate_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/dom_node_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/get_as_dom_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/get_parent_element_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/get_style_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/has_element_with_class_name_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/has_element_with_tag_name_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/insert_css_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/observe_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/parse_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/rename_element_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/resolve_list_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/sandbox_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/set_attributes_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/set_styles_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/table_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/dom/unwrap_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/editor_commands_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/editor_contenteditablemode_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/editor_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/incompatible_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/index.html create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/lang/array_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/lang/object_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/lang/string_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/quirks/clean_pasted_html_test.js create mode 100644 karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/test/undo_manager_test.js create mode 100644 karmaworld/apps/wysihtml5/templates/wysihtml5/widget.html create mode 100644 karmaworld/apps/wysihtml5/templatetags/__init__.py create mode 100644 karmaworld/apps/wysihtml5/templatetags/wysihtml5.py create mode 100644 karmaworld/apps/wysihtml5/widgets.py diff --git a/karmaworld/apps/notes/forms.py b/karmaworld/apps/notes/forms.py index df59404..3d1549a 100644 --- a/karmaworld/apps/notes/forms.py +++ b/karmaworld/apps/notes/forms.py @@ -1,26 +1,28 @@ #!/usr/bin/env python # -*- coding:utf8 -*- # Copyright (C) 2012 FinalsClub Foundation -from django.forms import ModelForm, IntegerField, HiddenInput, Form +from django.forms import ModelForm, IntegerField, HiddenInput, Form, CharField, Textarea from django.forms import TextInput from django_filepicker.widgets import FPFileWidget +from django.template.loader import render_to_string +from wysihtml5.widgets import RichTextEditor -from karmaworld.apps.notes.models import Note +from karmaworld.apps.notes.models import Note, NoteMarkdown class NoteForm(ModelForm): + html = CharField(widget=RichTextEditor) + class Meta: model = Note - fields = ('name', 'tags',) + fields = ('name', 'tags', 'html') widgets = { 'name': TextInput() } - class NoteDeleteForm(Form): note = IntegerField(widget=HiddenInput()) - class FileUploadForm(ModelForm): auto_id = False class Meta: diff --git a/karmaworld/apps/notes/migrations/0019_auto__add_field_notemarkdown_html.py b/karmaworld/apps/notes/migrations/0019_auto__add_field_notemarkdown_html.py new file mode 100644 index 0000000..7b72f21 --- /dev/null +++ b/karmaworld/apps/notes/migrations/0019_auto__add_field_notemarkdown_html.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'NoteMarkdown.html' + db.add_column(u'notes_notemarkdown', 'html', + self.gf('django.db.models.fields.TextField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'NoteMarkdown.html' + db.delete_column(u'notes_notemarkdown', 'html') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'courses.course': { + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'school'),)", 'object_name': 'Course'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.Department']", 'null': 'True', 'blank': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instructor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'instructor_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'professor': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['courses.Professor']", 'null': 'True', 'blank': 'True'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.School']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'thank_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + u'courses.department': { + 'Meta': {'unique_together': "(('name', 'school'),)", 'object_name': 'Department'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + u'courses.professor': { + 'Meta': {'unique_together': "(('name', 'email'),)", 'object_name': 'Professor'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'courses.school': { + 'Meta': {'ordering': "['-file_count', '-priority', 'name']", 'object_name': 'School'}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hashtag': ('django.db.models.fields.CharField', [], {'max_length': '16', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'priority': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'blank': 'True'}), + 'usde_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'licenses.license': { + 'Meta': {'object_name': 'License'}, + 'html': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}) + }, + u'notes.note': { + 'Meta': {'ordering': "['-uploaded_at']", 'unique_together': "(('fp_file', 'upstream_link'),)", 'object_name': 'Note'}, + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.Course']"}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'fp_file': ('django_filepicker.models.FPFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'gdrive_url': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['licenses.License']", 'null': 'True', 'blank': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'thanks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'tweeted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True'}), + 'upstream_link': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}) + }, + u'notes.notemarkdown': { + 'Meta': {'object_name': 'NoteMarkdown'}, + 'html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'markdown': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'note': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['notes.Note']", 'unique': 'True', 'primary_key': 'True'}) + }, + u'notes.useruploadmapping': { + 'Meta': {'unique_together': "(('user', 'fp_file'),)", 'object_name': 'UserUploadMapping'}, + 'fp_file': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['notes'] \ No newline at end of file diff --git a/karmaworld/apps/notes/migrations/0020_markdown_to_html.py b/karmaworld/apps/notes/migrations/0020_markdown_to_html.py new file mode 100644 index 0000000..c57881c --- /dev/null +++ b/karmaworld/apps/notes/migrations/0020_markdown_to_html.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +import markdown +from notes.models import NoteMarkdown + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + for notemarkdown in orm['notes.NoteMarkdown'].objects.exclude(markdown=""): + notemarkdown.html = NoteMarkdown.sanitize(markdown.markdown(notemarkdown.markdown)) + notemarkdown.save() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'courses.course': { + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'school'),)", 'object_name': 'Course'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.Department']", 'null': 'True', 'blank': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instructor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'instructor_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'professor': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['courses.Professor']", 'null': 'True', 'blank': 'True'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.School']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'thank_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + u'courses.department': { + 'Meta': {'unique_together': "(('name', 'school'),)", 'object_name': 'Department'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + u'courses.professor': { + 'Meta': {'unique_together': "(('name', 'email'),)", 'object_name': 'Professor'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'courses.school': { + 'Meta': {'ordering': "['-file_count', '-priority', 'name']", 'object_name': 'School'}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hashtag': ('django.db.models.fields.CharField', [], {'max_length': '16', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'priority': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'blank': 'True'}), + 'usde_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'licenses.license': { + 'Meta': {'object_name': 'License'}, + 'html': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}) + }, + u'notes.note': { + 'Meta': {'ordering': "['-uploaded_at']", 'unique_together': "(('fp_file', 'upstream_link'),)", 'object_name': 'Note'}, + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.Course']"}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'fp_file': ('django_filepicker.models.FPFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'gdrive_url': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['licenses.License']", 'null': 'True', 'blank': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'thanks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'tweeted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True'}), + 'upstream_link': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}) + }, + u'notes.notemarkdown': { + 'Meta': {'object_name': 'NoteMarkdown'}, + 'html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'markdown': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'note': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['notes.Note']", 'unique': 'True', 'primary_key': 'True'}) + }, + u'notes.useruploadmapping': { + 'Meta': {'unique_together': "(('user', 'fp_file'),)", 'object_name': 'UserUploadMapping'}, + 'fp_file': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['notes'] + symmetrical = True diff --git a/karmaworld/apps/notes/models.py b/karmaworld/apps/notes/models.py index c6c2a52..f6d9f03 100644 --- a/karmaworld/apps/notes/models.py +++ b/karmaworld/apps/notes/models.py @@ -33,6 +33,9 @@ from django.utils.text import slugify import django_filepicker from bs4 import BeautifulSoup as BS from taggit.managers import TaggableManager +import bleach +import bleach_whitelist +import markdown from karmaworld.apps.courses.models import Course from karmaworld.apps.licenses.models import License @@ -426,6 +429,20 @@ class Note(Document): class NoteMarkdown(models.Model): note = models.OneToOneField(Note, primary_key=True) markdown = models.TextField(blank=True, null=True) + html = models.TextField(blank=True, null=True) + + @classmethod + def sanitize(cls, html): + return bleach.clean(html, + bleach_whitelist.markdown_tags, + bleach_whitelist.markdown_attrs, + strip=True) + + def save(self, *args, **kwargs): + if self.markdown and not self.html: + self.html = markdown.markdown(self.markdown) + self.html = NoteMarkdown.sanitize(self.html) + super(NoteMarkdown, self).save(*args, **kwargs) auto_add_check_unique_together(Note) diff --git a/karmaworld/apps/notes/tests.py b/karmaworld/apps/notes/tests.py index eb06b85..64237d8 100644 --- a/karmaworld/apps/notes/tests.py +++ b/karmaworld/apps/notes/tests.py @@ -1,23 +1,14 @@ #!/usr/bin/env python # -*- coding:utf8 -*- # Copyright (C) 2012 FinalsClub Foundation -""" -""" -import karmaworld.secret.indexden as secret - import datetime from django.test import TestCase from karmaworld.apps.notes.search import SearchIndex -from karmaworld.apps.notes.models import Note +from karmaworld.apps.notes.models import Note, NoteMarkdown from karmaworld.apps.courses.models import Course from karmaworld.apps.courses.models import School -# Tell SearchIndex to put its entries -# in a separate index -secret.testing = True - - class TestNotes(TestCase): def setUp(self): @@ -65,7 +56,7 @@ class TestNotes(TestCase): self.note.save() self.assertEqual(self.note.slug, expected) - expected_url_prefix = u'/marshall-college/archaeology-101/' + expected_url_prefix = u'/note/marshall-college/archaeology-101/' expected_slug = u'lecture-notes-concerning-the-use-of-therefore' expected = expected_url_prefix + expected_slug @@ -78,3 +69,33 @@ class TestNotes(TestCase): self.note.slug = None url = self.expected_url_prefix + str(self.note.id) self.assertEqual(self.note.get_absolute_url(), url) + + def test_note_markdown_rendering(self): + rich = NoteMarkdown(note=self.note, + markdown="""# This is fun\n[oh](http://yeah.com)""") + rich.save() + self.assertEquals(rich.html, + """

This is fun

\n

oh

""") + + def test_note_rich_text_sanitization(self): + rich = NoteMarkdown(note=self.note, html=""" + +

Something

+

OK

+ & + ” + This stuff + That guy + """) + + rich.save() + self.assertEquals(rich.html, u""" + unsafe +

Something

+

OK

+ & + \u201d + This stuff + That guy + """) + diff --git a/karmaworld/apps/notes/views.py b/karmaworld/apps/notes/views.py index b6cdaf4..281c781 100644 --- a/karmaworld/apps/notes/views.py +++ b/karmaworld/apps/notes/views.py @@ -43,8 +43,13 @@ def note_page_context_helper(note, request, context): context['note_edit_form'] = NoteForm(request.POST) else: tags_string = ','.join([str(tag) for tag in note.tags.all()]) - context['note_edit_form'] = NoteForm(initial={'name': note.name, - 'tags': tags_string}) + initial = {"name": note.name, "tags": tags_string} + try: + initial["html"] = note.notemarkdown.html + except NoteMarkdown.DoesNotExist: + pass + print initial + context['note_edit_form'] = NoteForm(initial=initial) context['note_delete_form'] = NoteDeleteForm(initial={'note': note.id}) @@ -107,6 +112,22 @@ class NoteSaveView(FormView, SingleObjectMixin): } return super(NoteSaveView, self).get_context_data(**context) + def get_form_kwargs(self): + """ + Include related notemarkdown.html in form. + """ + kwargs = {"initial": self.get_initial()} + try: + kwargs["initial"]["html"] = self.object.notemarkdown.html + except NoteMarkdown.DoesNotExist: + pass + if self.request.method in ("POST", "PUT"): + kwargs.update({ + "data": self.request.POST, + "files": self.request.FILES, + }) + return kwargs + def form_valid(self, form): """ Actions to take if the submitted form is valid namely, saving the new data to the existing note object diff --git a/karmaworld/apps/wysihtml5/__init__.py b/karmaworld/apps/wysihtml5/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karmaworld/apps/wysihtml5/models.py b/karmaworld/apps/wysihtml5/models.py new file mode 100644 index 0000000..e69de29 diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/init.js b/karmaworld/apps/wysihtml5/static/wysihtml5/init.js new file mode 100644 index 0000000..18b8a57 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/init.js @@ -0,0 +1,17 @@ +function initWysihtml5(element) { + var editor = new wysihtml5.Editor(element, { + toolbar: element.id + "-toolbar", + parserRules: wysihtml5ParserRules + }); + editor.on("change", function() { element.value = editor.value; }); + return editor; +} + +(function() { + var elements = document.querySelectorAll("[role='wysihtml5-rich-text']"); + var elementArr = []; + for (var i = 0; i < elements.length; i++) { + elementArr.push(elements[i]); + } + elementArr.forEach(initWysihtml5); +})(); diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/test.html b/karmaworld/apps/wysihtml5/static/wysihtml5/test.html new file mode 100644 index 0000000..f92e308 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/test.html @@ -0,0 +1,20 @@ + + + + + + + +
+ bold + italic + H1 + P +
+ + + + + + + diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/toolbar.css b/karmaworld/apps/wysihtml5/static/wysihtml5/toolbar.css new file mode 100644 index 0000000..e69de29 diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/.gitignore b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/.gitignore new file mode 100644 index 0000000..af32fad --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/.gitignore @@ -0,0 +1,7 @@ +.tm_sync.config +.idea +.DS_Store +.DS_Store? +node_modules +local_assets +.tm_properties diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/CHANGELOG.textile b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/CHANGELOG.textile new file mode 100644 index 0000000..861ae37 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/CHANGELOG.textile @@ -0,0 +1,132 @@ +*wysihtml5x 0.4.17* (November 6, 2014) +* Updated rangy and added as node package +* Updated qUnit for testing +* Fixes some selection issues with webkit and autolinking +* Refactored composer.observer for readability + +*wysihtml5x 0.4.16* (October 23, 2014) +* Fix copy being broken for plain/text +* Change deleting and selection behaviour of uneditable containers + +*wysihtml5x 0.4.15* (September 17, 2014) +* Fixes copying incorrrect source (spans, styles etc.) from texteditor in webkit and adds ability to add separate ruleset for paste cleanup if source origin is editor itself + +*wysihtml5x 0.4.14* (September 10, 2014) +* Adds ability for check_attributes parameter in cleanup rules to define attributes whose name is beginning with string (ex: "data-*") +* Toolbar buttons active and disabled classes can be configured +* Paste logic rewrite. Pasted data is now precaught and separate rulesets can be defined for pasted data. + +*wysihtml5x 0.4.13* (August 11, 2014) +* Adds option for parser to keep comments +* Fixes list insertion problems in IE +* Fixes some leaking varaibles reduces browser error messages +* Updates rangy to latest (1.3alpha.20140804) + +*wysihtml5x 0.4.12* (July 03, 2014) +* Fixes some placeholder problems targeting Firefox and Safari. + +*wysihtml5x 0.4.11* (July 03, 2014) +* Tests are now made with up to date Qunit (tanks @Waxolunist) +* Fixes an error that placeholder will not be displayed when whitespace present in editor + +*wysihtml5x 0.4.10* (June 27, 2014) +* Solve inline formating issues when applying multiple styles +* Add a special case when deleting into heading with caret + +*wysihtml5x 0.4.9* (June 17, 2014) +* Prevent possible errors when uneditable element is passed through in parser +* getValue api changed so passing no parameter will parse and remove all possible internals on result. Takes two parameters getValue(true /* parse content */, true /* ensure removal of all internals and selection markers */) + +*wysihtml5x 0.4.8* (June 9, 2014) +*Improvements in list indent outdent handling +* Add insertBlockQuote command and remove blockquote creation from formatBlock + +*wysihtml5x 0.4.7* (June 2, 2014) +* Fixes mayor bug that prevents deleting some elements. + +*wysihtml5x 0.4.6* (June 2, 2014) +* Added "hasVisibleContent" to parser check methods to clean up empty elements +* Added list indent and outdent commands +* TAB key handling is now optional and can be turned off +* Minor bug fixes + +*wysihtml5x 0.4.5* (May 6, 2014) +* Added "add_style" to parser rules +* Added function "any" as parser class parameter to pass all classes +* Some errors fixed + +*wysihtml5x 0.4.4* (Apr 25, 2014) +* Adds TAB key support +* Improves list handling and adds possibility to create nested lists +* Improved behaviour of commands that rely on formatInline +* Added text alignment with style commands (alignLeftStyle, alignRightStyle, alignCenterStyle) +* Changes to grunt build rather than makefile +* Updated to newer rangy 1.3alpha +* Dropped Old Opera 12 support +* Minor bug fixes + +*wysihtml5x 0.4.3* (Mar 02, 2014) +* Adds command "formatCode" +* Fixes some IE8 mayor bugs + +*wysihtml5x 0.4.2* (Feb 13, 2014) +* Adds cell selection events "tableselectstart", "tableselectchange" +* Fixes configuration file issue of overriding link target attribute +* Add bower support + +*wysihtml5x 0.4.1* (Jan 28, 2014) +* "createTable" command has one additional parameter "tableStyle" accepting CSS string +* Selecting more than one table cell removes conflicting text selection +* Table api has now canMerge function for checking if cell merging is allowed on cells selected +* Method "any" added to Parser rules check_attributes enabling all attributes values to pass +* Various bugs fixed + +*wysihtml5x 0.4.0* (Dec 12, 2013) +* Changed syntax of background and foreground color with style commands +* Anchor creting and removing logic changed +* Default build is without internal toolbar functions and build with "-toolbar" suffix contains default toolbar functions + +*wysihtml5x 0.4.0-beta2* (Nov 7, 2013) +* Support for contenteditable without iframe sandbox when initiated on div instead of textarea +* Table creation and handling commands added +* Improved parser with options to: unwrap tag instead of remove, keep defined styles, complex object type definitions for allowing elements. +* Ability to add uneditable area inside editor text flow (useful when building modules like video tools, advanced image editor etc.) +* Improved formatblock handling +* Ability for user to remove formating with only collapsed caret. (without having to select exactly whole text) +* Minor performance fixes +* Ability to use inline styles if needed + +*wysihtml5 0.3.0* (May 18, 2012) + +* Support for query command groups in toolbar +* Don't overwrite title attribute on img and a tags (thx to @bobanj, #65) +* Fix copying of attributes (eg. spellcheck="false") +* Allow toolbar button other than anchors +* Fix another keyboard issue on Windows (#46) +* Improve insertUnorderedList and insertOrderedList commands +* Now possible to specify text of a hyperlink (thx to @jhollingworth, #45) + +*wysihtml5 0.3.0 RC 2* (March 29, 2012) + +* Changed License from GPL to MIT +* Now possible to use multiple editors on one page (#20) +* Fixed random IE error happening when retrieving currentStyle (#21) +* Fixed Safari context menu issue (#27) +* Fixed weird issue in Chrome happening when the editor was initialized when the textarea has been inserted dynamically (#15) +* Fixed issue happening with the polish keyboard layout (#16) +* Make sure that the editor iframe uses same IE document mode as it's parent + +*wysihtml5 0.3.0 RC 1* (February 5, 2012) + +* Basic iOS support (thx @javan for bearing with me) +* Proper undo/redo support for all browsers except IE +* Added more events: beforecommand:composer, aftercommand:composer, undo:composer, redo:composer, show:dialog, save:dialog, cancel:dialog (thx to @martinnormark) +* Fixed an annoying text pasting behavior +* Allow nested toolbar buttons (thx to @kagd) +* Updated rangy library to version 1.2.2 +* Improve handling of line breaks in list elements for even cleaner HTML output +* The editor now also copies border-radius styles from the textarea to the rich text element (thx to @henningthies) + +*wysihtml5 0.2.0* (July 13, 2011) + +* Library agnostic (no Prototype library needed anymore) diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/Gruntfile.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/Gruntfile.js new file mode 100644 index 0000000..8e77d3a --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/Gruntfile.js @@ -0,0 +1,155 @@ +module.exports = function(grunt) { + + "use strict"; + + // List required source files that will be built into wysihtml5x.js + var base = [ + "src/polyfills.js", + "src/wysihtml5.js", + "node_modules/rangy/lib/rangy-core.js", + "node_modules/rangy/lib/rangy-selectionsaverestore.js", + "lib/base/base.js", + "src/browser.js", + "src/lang/array.js", + "src/lang/dispatcher.js", + "src/lang/object.js", + "src/lang/string.js", + "src/dom/auto_link.js", + "src/dom/class.js", + "src/dom/contains.js", + "src/dom/convert_to_list.js", + "src/dom/copy_attributes.js", + "src/dom/copy_styles.js", + "src/dom/delegate.js", + "src/dom/dom_node.js", + "src/dom/get_as_dom.js", + "src/dom/get_parent_element.js", + "src/dom/get_style.js", + "src/dom/get_textnodes.js", + "src/dom/has_element_with_tag_name.js", + "src/dom/has_element_with_class_name.js", + "src/dom/insert.js", + "src/dom/insert_css.js", + "src/dom/line_breaks.js", + "src/dom/observe.js", + "src/dom/parse.js", + "src/dom/remove_empty_text_nodes.js", + "src/dom/rename_element.js", + "src/dom/replace_with_child_nodes.js", + "src/dom/resolve_list.js", + "src/dom/sandbox.js", + "src/dom/contenteditable_area.js", + "src/dom/set_attributes.js", + "src/dom/set_styles.js", + "src/dom/simulate_placeholder.js", + "src/dom/text_content.js", + "src/dom/get_attribute.js", + "src/dom/get_attributes.js", + "src/dom/is_loaded_image.js", + "src/dom/table.js", + "src/dom/query.js", + "src/dom/compare_document_position.js", + "src/dom/unwrap.js", + "src/dom/get_pasted_html.js", + "src/quirks/clean_pasted_html.js", + "src/quirks/ensure_proper_clearing.js", + "src/quirks/get_correct_inner_html.js", + "src/quirks/redraw.js", + "src/quirks/table_cells_selection.js", + "src/quirks/style_parser.js", + "src/selection/selection.js", + "src/selection/html_applier.js", + "src/commands.js", + "src/commands/bold.js", + "src/commands/createLink.js", + "src/commands/removeLink.js", + "src/commands/fontSize.js", + "src/commands/fontSizeStyle.js", + "src/commands/foreColor.js", + "src/commands/foreColorStyle.js", + "src/commands/bgColorStyle.js", + "src/commands/formatBlock.js", + "src/commands/formatCode.js", + "src/commands/formatInline.js", + "src/commands/insertBlockQuote.js", + "src/commands/insertHTML.js", + "src/commands/insertImage.js", + "src/commands/insertLineBreak.js", + "src/commands/insertOrderedList.js", + "src/commands/insertUnorderedList.js", + "src/commands/insertList.js", + "src/commands/italic.js", + "src/commands/justifyCenter.js", + "src/commands/justifyLeft.js", + "src/commands/justifyRight.js", + "src/commands/justifyFull.js", + "src/commands/alignRightStyle.js", + "src/commands/alignLeftStyle.js", + "src/commands/alignCenterStyle.js", + "src/commands/redo.js", + "src/commands/underline.js", + "src/commands/undo.js", + "src/commands/createTable.js", + "src/commands/mergeTableCells.js", + "src/commands/addTableCells.js", + "src/commands/deleteTableCells.js", + "src/commands/indentList.js", + "src/commands/outdentList.js", + "src/undo_manager.js", + "src/views/view.js", + "src/views/composer.js", + "src/views/composer.style.js", + "src/views/composer.observe.js", + "src/views/synchronizer.js", + "src/views/textarea.js", + "src/editor.js" + ]; + + // List of optional source files that will be built to wysihtml5x-toolbar.js + var toolbar = [ + "src/toolbar/dialog.js", + "src/toolbar/speech.js", + "src/toolbar/toolbar.js", + "src/toolbar/dialog_createTable.js", + "src/toolbar/dialog_foreColorStyle.js", + "src/toolbar/dialog_fontSizeStyle.js" + ]; + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + separator: ';', + process: function(src, filepath) { + return src.replace(/@VERSION/g, grunt.config.get('pkg.version')); + } + }, + dist: { + src: base, + dest: 'dist/<%= pkg.name %>.js' + }, + toolbar: { + src: base.concat(toolbar), + dest: 'dist/<%= pkg.name %>-toolbar.js' + } + }, + uglify: { + options: { + banner: '/*! <%= pkg.name %> - v<%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd") %>) */\n', + sourceMap: true + }, + build: { + files: { + 'dist/<%= pkg.name %>.min.js': 'dist/<%= pkg.name %>.js', + 'dist/<%= pkg.name %>-toolbar.min.js': 'dist/<%= pkg.name %>-toolbar.js' + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + + grunt.registerTask('default', ['concat', 'uglify']); +}; diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/LICENSE b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/LICENSE new file mode 100644 index 0000000..e5083d0 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2012 XING AG + +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. \ No newline at end of file diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/README.markdown b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/README.markdown new file mode 100644 index 0000000..59920e5 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/README.markdown @@ -0,0 +1,66 @@ +# wysihtml5x + +wysihtml5x is an extended and less strict approach on xing/wysihtml5 open source rich text editor based on HTML5 technology. +The code is completely library agnostic: No jQuery, Prototype or similar is required. + +This project was initiated and is supported by the [XING AG](https://www.xing.com). Thanks! + +## Features + +* Auto linking of urls as-you-type +* Generates valid and semantic HTML5 markup (no `` tags) +* Can use class-names instead of inline styles +* Unifies line-break handling across browsers (hitting enter will create `
` instead of `

` or `

`) +* Auto-parses content inserted via copy & paste (from Word, Powerpoint, PDF, other web pages, ...) +* Converts invalid or unknown html tags into valid/similar tags +* Source code view for users with HTML skills +* Uses sandboxed iframes in order to prevent identity theft through XSS +* Editor inherits styles and attributes (placeholder, autofocus, ...) from original textarea (you only have to style one element) +* Speech-input for Chrome + +Extended features not present in xing/wysihtml5: + +* Can be used without iframe sandbox when initiated on div instead of textarea +* Blocking of image drag drop in editable is removed +* Table insertion management and cell merging commands +* Improved parser with options to: unwrap tag instead of remove, keep defined styles, complex object type definitions for allowing elements. +* Ability to add uneditable area inside editor text flow (useful when building modules like video tools, advanced image editor etc.) +* Improved formatblock handling +* Ability for user to remove formating with only collapsed caret. (without having to select exactly whole text) +* Improved speed +* Anchor creting and removing logic changed to more universal +* Default build is without internal toolbar functions and build with "-toolbar" suffix contains default toolbar functions + +## Browser Support + +The rich text editing interface is supported in IE8+, FF 3.5+, Safari 4+, Safari on iOS 5+, Opera 11+ and Chrome. +**Graceful Degradation:** Users with other browsers will see the textarea and are still able to write plain HTML by themselves. + +## Companies using wysihtml5 + +* [Basecamp](http://basecamp.com) - Leading web-based project management and collaboration tool +* [XING](https://www.xing.com) - Business Social Network with more than 12 million members +* [Qype](http://www.qype.com) - Largest user-generated local review site in Europe +* and many more ... + +## Research + +Before starting wysihtml5 we spent a lot of time investigating the different browsers and their behaviors. + +Check this repository: https://github.com/tiff/wysihtml5-tests + +A compatibility table for rich text query commands can be found here: http://tifftiff.de/contenteditable/compliance_test.html + +A pure native rich text editor with HTML validator and live source preview is here: http://tifftiff.de/contenteditable/editor.html + +## Development + +wysihtml5 can be built using Grunt. Installation instructions for [Grunt can be found here](http://gruntjs.com/getting-started). Once you have it installed, wysihtml5 can be built by simply running + + grunt + +This builds both minified and development versions, including one with toolbar support. + +## Contributors + +See the [list of contributors here](https://github.com/Edicy/wysihtml5/graphs/contributors). diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/bower.json b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/bower.json new file mode 100644 index 0000000..b87c9ec --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/bower.json @@ -0,0 +1,28 @@ +{ + "name": "wysihtml5x", + "version": "0.4.17", + "main": [ + "dist/wysihtml5x.min.js", + "dist/wysihtml5x-toolbar.min.js" + ], + "dependencies": { + }, + "homepage": "https://github.com/Edicy/wysihtml5", + "authors": [ + "edicy", + "xing" + ], + "description": "wysihtml5x is an extended and less strict approach on xing/wysihtml5 open source rich text editor based on HTML5 technology.", + "keywords": [ + "wysiwyg" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "build", + "examples", + "test", + "Makefile", + "src" + ] +} diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.js new file mode 100644 index 0000000..b298bde --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/dist/wysihtml5x-toolbar.js @@ -0,0 +1,14508 @@ +// TODO: in future try to replace most inline compability checks with polyfills for code readability + +// IE8 SUPPORT BLOCK +// You can compile wuthout all this if IE8 is not needed + +// addEventListener, removeEventListener +// TODO: make usage of wysihtml5.dom.observe obsolete +(function() { + if (!Event.prototype.preventDefault) { + Event.prototype.preventDefault=function() { + this.returnValue=false; + }; + } + if (!Event.prototype.stopPropagation) { + Event.prototype.stopPropagation=function() { + this.cancelBubble=true; + }; + } + if (!Element.prototype.addEventListener) { + var eventListeners=[]; + + var addEventListener=function(type,listener /*, useCapture (will be ignored) */) { + var self=this; + var wrapper=function(e) { + e.target=e.srcElement; + e.currentTarget=self; + if (listener.handleEvent) { + listener.handleEvent(e); + } else { + listener.call(self,e); + } + }; + if (type=="DOMContentLoaded") { + var wrapper2=function(e) { + if (document.readyState=="complete") { + wrapper(e); + } + }; + document.attachEvent("onreadystatechange",wrapper2); + eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper2}); + + if (document.readyState=="complete") { + var e=new Event(); + e.srcElement=window; + wrapper2(e); + } + } else { + this.attachEvent("on"+type,wrapper); + eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper}); + } + }; + var removeEventListener=function(type,listener /*, useCapture (will be ignored) */) { + var counter=0; + while (counter -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + ++i; + } + return i; + } + + function getNodeLength(node) { + switch (node.nodeType) { + case 7: + case 10: + return 0; + case 3: + case 8: + return node.length; + default: + return node.childNodes.length; + } + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function isTextOrCommentNode(node) { + if (!node) { + return false; + } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index, positionsToPreserve) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + + // Preserve positions + if (positionsToPreserve) { + for (var i = 0, position; position = positionsToPreserve[i++]; ) { + // Handle case where position was inside the portion of node after the split point + if (position.node == node && position.offset > index) { + position.node = newNode; + position.offset -= index; + } + // Handle the case where the position is a node offset within node's parent + else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { + ++position.offset; + } + } + } + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw module.createError("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw module.createError("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw module.createError("getIframeDocument: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw module.createError("getIframeWindow: No Window object found for iframe element"); + } + } + + // This looks bad. Is it worth it? + function isWindow(obj) { + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + } + + function getContentDocument(obj, module, methodName) { + var doc; + + if (!obj) { + doc = document; + } + + // Test if a DOM node has been passed and obtain a document object for it if so + else if (util.isHostProperty(obj, "nodeType")) { + doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? + getIframeDocument(obj) : getDocument(obj); + } + + // Test if the doc parameter appears to be a Window object + else if (isWindow(obj)) { + doc = obj.document; + } + + if (!doc) { + throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); + } + + return doc; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + root = getCommonAncestor(nodeA, nodeB); + if (!root) { + throw new Error("comparePoints error: nodes have no common ancestor"); + } + + // Case 4: containers are siblings or descendants of siblings + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + } + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried + var crashyTextNodes = false; + + function isBrokenNode(node) { + var n; + try { + n = node.parentNode; + return false; + } catch (e) { + return true; + } + } + + (function() { + var el = document.createElement("b"); + el.innerHTML = "1"; + var textNode = el.firstChild; + el.innerHTML = "
"; + crashyTextNodes = isBrokenNode(textNode); + + api.features.crashyTextNodes = crashyTextNodes; + })(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (crashyTextNodes && isBrokenNode(node)) { + return "[Broken node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } + if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; + } + return node.nodeName; + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + var getComputedStyleProperty; + if (typeof window.getComputedStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return getWindow(el).getComputedStyle(el, null)[propName]; + }; + } else if (typeof document.documentElement.currentStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return el.currentStyle[propName]; + }; + } else { + module.fail("No means of obtaining computed style properties found"); + } + + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return !!pos && this.node === pos.node && this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + }, + + toString: function() { + return this.inspect(); + } + }; + + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11, + INVALID_NODE_TYPE_ERR: 24 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + isOrIsAncestorOf: isOrIsAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + isTextOrCommentNode: isTextOrCommentNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: util.getBody, + isWindow: isWindow, + getContentDocument: getContentDocument, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + isBrokenNode: isBrokenNode, + inspectNode: inspectNode, + getComputedStyleProperty: getComputedStyleProperty, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Pure JavaScript implementation of DOM Range + api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + var isCharacterDataNode = dom.isCharacterDataNode; + var getNodeIndex = dom.getNodeIndex; + var isOrIsAncestorOf = dom.isOrIsAncestorOf; + var getDocument = dom.getDocument; + var comparePoints = dom.comparePoints; + var splitDataNode = dom.splitDataNode; + var getClosestAncestorIn = dom.getClosestAncestorIn; + var getNodeLength = dom.getNodeLength; + var arrayContains = dom.arrayContains; + var getRootContainer = dom.getRootContainer; + var crashyTextNodes = api.features.crashyTextNodes; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); + } + + function getRangeDocument(range) { + return range.document || getDocument(range.startContainer); + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { + assertRangeValid(rangeA); + assertRangeValid(rangeB); + + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + if (rangeIterator.isPartiallySelectedSubtree()) { + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of + // the node selected by the Range. + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendants + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if (filterNodeTypes && !regex.test(node.nodeType)) { + return; + } + if (filterExists && !filter(node)) { + return; + } + // Don't include a boundary container if it is a character data node and the range does not contain any + // of its character data. See issue 190. + var sc = range.startContainer; + if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { + return; + } + + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } + + nodes.push(node); + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); + } + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(false); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); + + if (isOrIsAncestorOf(current, this.sc)) { + startContainer = this.sc; + startOffset = this.so; + } + if (isOrIsAncestorOf(current, this.ec)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function() { + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isOrphan(node) { + return (crashyTextNodes && dom.isBrokenNode(node)) || + !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } + + function isValidOffset(node, offset) { + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer && + !isOrphan(range.startContainer) && + !isOrphan(range.endContainer) && + isValidOffset(range.startContainer, range.startOffset) && + isValidOffset(range.endContainer, range.endOffset)); + } + + function assertRangeValid(range) { + if (!isRangeValid(range)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" && + dom.isHtmlNamespace(getDocument(el).documentElement) && + dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + function splitRangeBoundaries(range, positionsToPreserve) { + assertRangeValid(range); + + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; + var startEndSame = (sc === ec); + + if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + splitDataNode(ec, eo, positionsToPreserve); + } + + if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { + sc = splitDataNode(sc, so, positionsToPreserve); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { + eo++; + } + so = 0; + } + range.setStartAndEnd(sc, so, ec, eo); + } + + function rangeToHtml(range) { + assertRangeValid(range); + var container = range.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild( range.cloneContents() ); + return container.innerHTML; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + util.extend(api.rangePrototype, { + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (isOrIsAncestorOf(node, this.startContainer)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textParts = [], iterator = new RangeIterator(this, true); + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + if (node.nodeType == 3 || node.nodeType == 4) { + textParts.push(node.data); + } + }); + iterator.detach(); + return textParts.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + return rangeToHtml(this); + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (getDocument(node) !== getRangeDocument(this)) { + return false; + } + + var parent = node.parentNode, offset = getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); + + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range) { + return rangesIntersect(this, range, false); + }, + + // Sharing a boundary start-to-end or end-to-start does count as intersection. + intersectsOrTouchesRange: function(range) { + return rangesIntersect(this, range, true); + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsOrTouchesRange(range)) { + var unionRange = this.cloneRange(); + if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new DOMException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + var intersection = this.intersection(range); + return intersection !== null && range.equals(intersection); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + return this.containsRange(nodeRange); + } else { + return this.containsNodeContents(node); + } + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + this.setStartAfter(node); + this.collapse(true); + }, + + getBookmark: function(containerNode) { + var doc = getRangeDocument(this); + var preSelectionRange = api.createRange(doc); + containerNode = containerNode || dom.getBody(doc); + preSelectionRange.selectNodeContents(containerNode); + var range = this.intersection(preSelectionRange); + var start = 0, end = 0; + if (range) { + preSelectionRange.setEnd(range.startContainer, range.startOffset); + start = preSelectionRange.toString().length; + end = start + range.toString().length; + } + + return { + start: start, + end: end, + containerNode: containerNode + }; + }, + + moveToBookmark: function(bookmark) { + var containerNode = bookmark.containerNode; + var charIndex = 0; + this.setStart(containerNode, 0); + this.collapse(true); + var nodeStack = [containerNode], node, foundStart = false, stop = false; + var nextCharIndex, i, childNodes; + + while (!stop && (node = nodeStack.pop())) { + if (node.nodeType == 3) { + nextCharIndex = charIndex + node.length; + if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { + this.setStart(node, bookmark.start - charIndex); + foundStart = true; + } + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { + this.setEnd(node, bookmark.end - charIndex); + stop = true; + } + charIndex = nextCharIndex; + } else { + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); + } + } + } + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + }, + + detach: function() { + // In DOM4, detach() is now a no-op. + } + }); + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + // Set up inheritance + var F = function() {}; + F.prototype = api.rangePrototype; + constructor.prototype = new F(); + + util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + /** + * Convenience method to set a range's start and end boundaries. Overloaded as follows: + * - Two parameters (node, offset) creates a collapsed range at that position + * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at + * startOffset and ending at endOffset + * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in + * startNode and ending at endOffset in endNode + */ + setStartAndEnd: function() { + var args = arguments; + var sc = args[0], so = args[1], ec = sc, eo = so; + + switch (args.length) { + case 3: + eo = args[2]; + break; + case 4: + ec = args[2]; + eo = args[3]; + break; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + setBoundary: function(node, offset, isStart) { + this["set" + (isStart ? "Start" : "End")](node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, + + selectNode: function(node) { + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + splitBoundaries: function() { + splitRangeBoundaries(this); + }, + + splitBoundariesPreservingPositions: function(positionsToPreserve) { + splitRangeBoundaries(this, positionsToPreserve); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + sibling.parentNode.removeChild(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + sibling.parentNode.removeChild(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + + if (isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + this.setStartAndEnd(node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + range.document = dom.getDocument(startContainer); + + updateCollapsedAndCommonAncestor(range); + } + + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries); + + util.extend(Range, { + rangeProperties: rangeProperties, + RangeIterator: RangeIterator, + copyComparisonConstants: copyComparisonConstants, + createPrototypeRange: createPrototypeRange, + inspect: inspect, + toHtml: rangeToHtml, + getRangeDocument: getRangeDocument, + rangesEqual: function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + } + }); + + api.DomRange = Range; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wrappers for the browser's native DOM Range and/or TextRange implementation + api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { + var WrappedRange, WrappedTextRange; + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + var getBody = dom.getBody; + var getContentDocument = dom.getContentDocument; + var isCharacterDataNode = dom.isCharacterDataNode; + + + /*----------------------------------------------------------------------------------------------------------------*/ + + if (api.features.implementsDomRange) { + // This is a wrapper around the browser's native DOM Range. It has two aims: + // - Provide workarounds for specific browser bugs + // - provide convenient extensions, which are inherited from Rangy's DomRange + + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; + + function updateRangeProperties(range) { + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = range.nativeRange[prop]; + } + // Fix for broken collapsed property in IE 9. + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + } + + function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); + var nativeRangeDifferent = !range.equals(range.nativeRange); + + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved || nativeRangeDifferent) { + range.setEnd(endContainer, endOffset); + range.setStart(startContainer, startOffset); + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw module.createError("WrappedRange: Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + + rangeProto = WrappedRange.prototype; + + rangeProto.selectNode = function(node) { + this.nativeRange.selectNode(node); + updateRangeProperties(this); + }; + + rangeProto.cloneContents = function() { + return this.nativeRange.cloneContents(); + }; + + // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, + // insertNode() is never delegated to the native range. + + rangeProto.surroundContents = function(node) { + this.nativeRange.surroundContents(node); + updateRangeProperties(this); + }; + + rangeProto.collapse = function(isStart) { + this.nativeRange.collapse(isStart); + updateRangeProperties(this); + }; + + rangeProto.cloneRange = function() { + return new WrappedRange(this.nativeRange.cloneRange()); + }; + + rangeProto.refresh = function() { + updateRangeProperties(this); + }; + + rangeProto.toString = function() { + return this.nativeRange.toString(); + }; + + // Create test range and node for feature detection + + var testTextNode = document.createTextNode("test"); + getBody(document).appendChild(testTextNode); + var range = document.createRange(); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and + // correct for it + + range.setStart(testTextNode, 0); + range.setEnd(testTextNode, 0); + + try { + range.setStart(testTextNode, 1); + + rangeProto.setStart = function(node, offset) { + this.nativeRange.setStart(node, offset); + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + this.nativeRange.setEnd(node, offset); + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name) { + return function(node) { + this.nativeRange[name](node); + updateRangeProperties(this); + }; + }; + + } catch(ex) { + + rangeProto.setStart = function(node, offset) { + try { + this.nativeRange.setStart(node, offset); + } catch (ex) { + this.nativeRange.setEnd(node, offset); + this.nativeRange.setStart(node, offset); + } + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + try { + this.nativeRange.setEnd(node, offset); + } catch (ex) { + this.nativeRange.setStart(node, offset); + this.nativeRange.setEnd(node, offset); + } + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name, oppositeName) { + return function(node) { + try { + this.nativeRange[name](node); + } catch (ex) { + this.nativeRange[oppositeName](node); + this.nativeRange[name](node); + } + updateRangeProperties(this); + }; + }; + } + + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing + // whether the native implementation can be trusted + rangeProto.selectNodeContents = function(node) { + this.setStartAndEnd(node, 0, dom.getNodeLength(node)); + }; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for + // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 + + range.selectNodeContents(testTextNode); + range.setEnd(testTextNode, 3); + + var range2 = document.createRange(); + range2.selectNodeContents(testTextNode); + range2.setEnd(testTextNode, 4); + range2.setStart(testTextNode, 2); + + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { + // This is the wrong way round, so correct for it + + rangeProto.compareBoundaryPoints = function(type, range) { + range = range.nativeRange || range; + if (type == range.START_TO_END) { + type = range.END_TO_START; + } else if (type == range.END_TO_START) { + type = range.START_TO_END; + } + return this.nativeRange.compareBoundaryPoints(type, range); + }; + } else { + rangeProto.compareBoundaryPoints = function(type, range) { + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. + + var el = document.createElement("div"); + el.innerHTML = "123"; + var textNode = el.firstChild; + var body = getBody(document); + body.appendChild(el); + + range.setStart(textNode, 1); + range.setEnd(textNode, 2); + range.deleteContents(); + + if (textNode.data == "13") { + // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and + // extractContents() + rangeProto.deleteContents = function() { + this.nativeRange.deleteContents(); + updateRangeProperties(this); + }; + + rangeProto.extractContents = function() { + var frag = this.nativeRange.extractContents(); + updateRangeProperties(this); + return frag; + }; + } else { + } + + body.removeChild(el); + body = null; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for existence of createContextualFragment and delegate to it if it exists + if (util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Clean up + getBody(document).removeChild(testTextNode); + + rangeProto.getName = function() { + return "WrappedRange"; + }; + + api.WrappedRange = WrappedRange; + + api.createNativeRange = function(doc) { + doc = getContentDocument(doc, module, "createNativeRange"); + return doc.createRange(); + }; + })(); + } + + if (api.features.implementsTextRange) { + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + +
  • | a
  • b |
+ + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + var getTextRangeContainerElement = function(textRange) { + var parentEl = textRange.parentElement(); + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + }; + + var textRangeIsCollapsed = function(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + }; + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started + // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) + // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange + // bugs, handling for inputs and images, plus optimizations. + var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { + var workingRange = textRange.duplicate(); + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { + containerElement = wholeRangeContainerElement; + } + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + return { + boundaryPosition: pos, + nodeInfo: { + nodeIndex: pos.offset, + containerElement: pos.node + } + }; + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + + // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 + // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 + if (workingNode.parentNode) { + workingNode.parentNode.removeChild(workingNode); + } + + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; + var childNodeCount = containerElement.childNodes.length; + var end = childNodeCount; + + // Check end first. Code within the loop assumes that the endth child node of the container is definitely + // after the range boundary. + var nodeIndex = end; + + while (true) { + if (nodeIndex == childNodeCount) { + containerElement.appendChild(workingNode); + } else { + containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); + } + workingRange.moveToElementText(workingNode); + comparison = workingRange.compareEndPoints(workingComparisonType, textRange); + if (comparison == 0 || start == end) { + break; + } else if (comparison == -1) { + if (end == start + 1) { + // We know the endth child node is after the range boundary, so we must be done. + break; + } else { + start = nodeIndex; + } + } else { + end = (end == start + 1) ? start : nodeIndex; + } + nodeIndex = Math.floor((start + end) / 2); + containerElement.removeChild(workingNode); + } + + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of + // the node containing the text range's boundary, so we move the end of the working range to the + // boundary point and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing rendered line breaks (within a +
 element, for example), we need a slightly complicated approach to get the boundary's offset in
+                        IE. The facts:
+                        
+                        - Each line break is represented as \r in the text node's data/nodeValue properties
+                        - Each line break is represented as \r\n in the TextRange's 'text' property
+                        - The 'text' property of the TextRange does not contain trailing line breaks
+                        
+                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
+                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
+                        to use this to store the characters moved when moving both the start and end of the range to the
+                        start of the document body and subtracting the start offset from the end offset (the
+                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
+                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
+                        the end of the document) has the same problem.
+                        
+                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
+                        end boundary one character at a time and incrementing a counter with the value returned by the
+                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
+                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
+                        by the location of the range within the document).
+                        
+                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
+                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
+                        be longer than the text of the TextRange, so the start of the range is moved that length initially
+                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
+                        property. This has good performance in most situations compared to the previous two methods.
+                        */
+                        var tempRange = workingRange.duplicate();
+                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                        offset = tempRange.moveStart("character", rangeLength);
+                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                            offset++;
+                            tempRange.moveStart("character", 1);
+                        }
+                    } else {
+                        offset = workingRange.text.length;
+                    }
+                    boundaryPosition = new DomPosition(boundaryNode, offset);
+                } else {
+
+                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                    // a position within that, and likewise for a start boundary preceding a character data node
+                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                    if (nextNode && isCharacterDataNode(nextNode)) {
+                        boundaryPosition = new DomPosition(nextNode, 0);
+                    } else if (previousNode && isCharacterDataNode(previousNode)) {
+                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                    } else {
+                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                    }
+                }
+
+                // Clean up
+                workingNode.parentNode.removeChild(workingNode);
+
+                return {
+                    boundaryPosition: boundaryPosition,
+                    nodeInfo: {
+                        nodeIndex: nodeIndex,
+                        containerElement: containerElement
+                    }
+                };
+            };
+
+            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
+            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+            // (http://code.google.com/p/ierange/)
+            var createBoundaryTextRange = function(boundaryPosition, isStart) {
+                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+                var doc = dom.getDocument(boundaryPosition.node);
+                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+                if (nodeIsDataNode) {
+                    boundaryNode = boundaryPosition.node;
+                    boundaryParent = boundaryNode.parentNode;
+                } else {
+                    childNodes = boundaryPosition.node.childNodes;
+                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                    boundaryParent = boundaryPosition.node;
+                }
+
+                // Position the range immediately before the node containing the boundary
+                workingNode = doc.createElement("span");
+
+                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
+                // the element rather than immediately before or after it
+                workingNode.innerHTML = "&#feff;";
+
+                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+                if (boundaryNode) {
+                    boundaryParent.insertBefore(workingNode, boundaryNode);
+                } else {
+                    boundaryParent.appendChild(workingNode);
+                }
+
+                workingRange.moveToElementText(workingNode);
+                workingRange.collapse(!isStart);
+
+                // Clean up
+                boundaryParent.removeChild(workingNode);
+
+                // Move the working range to the text offset, if required
+                if (nodeIsDataNode) {
+                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                }
+
+                return workingRange;
+            };
+
+            /*------------------------------------------------------------------------------------------------------------*/
+
+            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+            // prototype
+
+            WrappedTextRange = function(textRange) {
+                this.textRange = textRange;
+                this.refresh();
+            };
+
+            WrappedTextRange.prototype = new DomRange(document);
+
+            WrappedTextRange.prototype.refresh = function() {
+                var start, end, startBoundary;
+
+                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+                if (textRangeIsCollapsed(this.textRange)) {
+                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                        true).boundaryPosition;
+                } else {
+                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                    start = startBoundary.boundaryPosition;
+
+                    // An optimization used here is that if the start and end boundaries have the same parent element, the
+                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                    // the start boundary
+                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                        startBoundary.nodeInfo).boundaryPosition;
+                }
+
+                this.setStart(start.node, start.offset);
+                this.setEnd(end.node, end.offset);
+            };
+
+            WrappedTextRange.prototype.getName = function() {
+                return "WrappedTextRange";
+            };
+
+            DomRange.copyComparisonConstants(WrappedTextRange);
+
+            var rangeToTextRange = function(range) {
+                if (range.collapsed) {
+                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                } else {
+                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                    textRange.setEndPoint("StartToStart", startRange);
+                    textRange.setEndPoint("EndToEnd", endRange);
+                    return textRange;
+                }
+            };
+
+            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+
+            WrappedTextRange.prototype.toTextRange = function() {
+                return rangeToTextRange(this);
+            };
+
+            api.WrappedTextRange = WrappedTextRange;
+
+            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+            // implementation to use by default.
+            if (!api.features.implementsDomRange || api.config.preferTextRange) {
+                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+                var globalObj = (function(f) { return f("return this;")(); })(Function);
+                if (typeof globalObj.Range == "undefined") {
+                    globalObj.Range = WrappedTextRange;
+                }
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return getBody(doc).createTextRange();
+                };
+
+                api.WrappedRange = WrappedTextRange;
+            }
+        }
+
+        api.createRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRange");
+            return new api.WrappedRange(api.createNativeRange(doc));
+        };
+
+        api.createRangyRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRangyRange");
+            return new DomRange(doc);
+        };
+
+        api.createIframeRange = function(iframeEl) {
+            module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
+            return api.createRange(iframeEl);
+        };
+
+        api.createIframeRangyRange = function(iframeEl) {
+            module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
+            return api.createRangyRange(iframeEl);
+        };
+
+        api.addShimListener(function(win) {
+            var doc = win.document;
+            if (typeof doc.createRange == "undefined") {
+                doc.createRange = function() {
+                    return api.createRange(doc);
+                };
+            }
+            doc = win = null;
+        });
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
+    // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
+    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
+        api.config.checkSelectionRanges = true;
+
+        var BOOLEAN = "boolean";
+        var NUMBER = "number";
+        var dom = api.dom;
+        var util = api.util;
+        var isHostMethod = util.isHostMethod;
+        var DomRange = api.DomRange;
+        var WrappedRange = api.WrappedRange;
+        var DOMException = api.DOMException;
+        var DomPosition = dom.DomPosition;
+        var getNativeSelection;
+        var selectionIsCollapsed;
+        var features = api.features;
+        var CONTROL = "Control";
+        var getDocument = dom.getDocument;
+        var getBody = dom.getBody;
+        var rangesEqual = DomRange.rangesEqual;
+
+
+        // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
+        // Boolean (true for backwards).
+        function isDirectionBackward(dir) {
+            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
+        }
+
+        function getWindow(win, methodName) {
+            if (!win) {
+                return window;
+            } else if (dom.isWindow(win)) {
+                return win;
+            } else if (win instanceof WrappedSelection) {
+                return win.win;
+            } else {
+                var doc = dom.getContentDocument(win, module, methodName);
+                return dom.getWindow(doc);
+            }
+        }
+
+        function getWinSelection(winParam) {
+            return getWindow(winParam, "getWinSelection").getSelection();
+        }
+
+        function getDocSelection(winParam) {
+            return getWindow(winParam, "getDocSelection").document.selection;
+        }
+        
+        function winSelectionIsBackward(sel) {
+            var backward = false;
+            if (sel.anchorNode) {
+                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backward;
+        }
+
+        // Test for the Range/TextRange and Selection features required
+        // Test for ability to retrieve selection
+        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
+            implementsDocSelection = util.isHostObject(document, "selection");
+
+        features.implementsWinGetSelection = implementsWinGetSelection;
+        features.implementsDocSelection = implementsDocSelection;
+
+        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+        if (useDocumentSelection) {
+            getNativeSelection = getDocSelection;
+            api.isSelectionValid = function(winParam) {
+                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+
+                // Check whether the selection TextRange is actually contained within the correct document
+                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+            };
+        } else if (implementsWinGetSelection) {
+            getNativeSelection = getWinSelection;
+            api.isSelectionValid = function() {
+                return true;
+            };
+        } else {
+            module.fail("Neither document.selection or window.getSelection() detected.");
+        }
+
+        api.getNativeSelection = getNativeSelection;
+
+        var testSelection = getNativeSelection();
+        var testRange = api.createNativeRange(document);
+        var body = getBody(document);
+
+        // Obtaining a range from a selection
+        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
+            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
+
+        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+        // Test for existence of native selection extend() method
+        var selectionHasExtend = isHostMethod(testSelection, "extend");
+        features.selectionHasExtend = selectionHasExtend;
+        
+        // Test if rangeCount exists
+        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
+        features.selectionHasRangeCount = selectionHasRangeCount;
+
+        var selectionSupportsMultipleRanges = false;
+        var collapsedNonEditableSelectionsSupported = true;
+
+        var addRangeBackwardToNative = selectionHasExtend ?
+            function(nativeSelection, range) {
+                var doc = DomRange.getRangeDocument(range);
+                var endRange = api.createRange(doc);
+                endRange.collapseToPoint(range.endContainer, range.endOffset);
+                nativeSelection.addRange(getNativeRange(endRange));
+                nativeSelection.extend(range.startContainer, range.startOffset);
+            } : null;
+
+        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
+
+            (function() {
+                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
+                // performed on the current document's selection. See issue 109.
+
+                // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
+                // because initialization usually happens when the document loads, but could be a problem for a script that
+                // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
+                // selection.
+                var sel = window.getSelection();
+                if (sel) {
+                    // Store the current selection
+                    var originalSelectionRangeCount = sel.rangeCount;
+                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
+                    var originalSelectionRanges = [];
+                    var originalSelectionBackward = winSelectionIsBackward(sel); 
+                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
+                        originalSelectionRanges[i] = sel.getRangeAt(i);
+                    }
+                    
+                    // Create some test elements
+                    var body = getBody(document);
+                    var testEl = body.appendChild( document.createElement("div") );
+                    testEl.contentEditable = "false";
+                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
+
+                    // Test whether the native selection will allow a collapsed selection within a non-editable element
+                    var r1 = document.createRange();
+
+                    r1.setStart(textNode, 1);
+                    r1.collapse(true);
+                    sel.addRange(r1);
+                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+                    sel.removeAllRanges();
+
+                    // Test whether the native selection is capable of supporting multiple ranges.
+                    if (!selectionHasMultipleRanges) {
+                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
+                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
+                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
+                        // sniff. I'm not happy about it. See
+                        // https://code.google.com/p/chromium/issues/detail?id=399791
+                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
+                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
+                            selectionSupportsMultipleRanges = false;
+                        } else {
+                            var r2 = r1.cloneRange();
+                            r1.setStart(textNode, 0);
+                            r2.setEnd(textNode, 3);
+                            r2.setStart(textNode, 2);
+                            sel.addRange(r1);
+                            sel.addRange(r2);
+                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+                        }
+                    }
+
+                    // Clean up
+                    body.removeChild(testEl);
+                    sel.removeAllRanges();
+
+                    for (i = 0; i < originalSelectionRangeCount; ++i) {
+                        if (i == 0 && originalSelectionBackward) {
+                            if (addRangeBackwardToNative) {
+                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
+                            } else {
+                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
+                                sel.addRange(originalSelectionRanges[i]);
+                            }
+                        } else {
+                            sel.addRange(originalSelectionRanges[i]);
+                        }
+                    }
+                }
+            })();
+        }
+
+        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+        // ControlRanges
+        var implementsControlRange = false, testControlRange;
+
+        if (body && isHostMethod(body, "createControlRange")) {
+            testControlRange = body.createControlRange();
+            if (util.areHostProperties(testControlRange, ["item", "add"])) {
+                implementsControlRange = true;
+            }
+        }
+        features.implementsControlRange = implementsControlRange;
+
+        // Selection collapsedness
+        if (selectionHasAnchorAndFocus) {
+            selectionIsCollapsed = function(sel) {
+                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+            };
+        } else {
+            selectionIsCollapsed = function(sel) {
+                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+            };
+        }
+
+        function updateAnchorAndFocusFromRange(sel, range, backward) {
+            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
+            sel.anchorNode = range[anchorPrefix + "Container"];
+            sel.anchorOffset = range[anchorPrefix + "Offset"];
+            sel.focusNode = range[focusPrefix + "Container"];
+            sel.focusOffset = range[focusPrefix + "Offset"];
+        }
+
+        function updateAnchorAndFocusFromNativeSelection(sel) {
+            var nativeSel = sel.nativeSelection;
+            sel.anchorNode = nativeSel.anchorNode;
+            sel.anchorOffset = nativeSel.anchorOffset;
+            sel.focusNode = nativeSel.focusNode;
+            sel.focusOffset = nativeSel.focusOffset;
+        }
+
+        function updateEmptySelection(sel) {
+            sel.anchorNode = sel.focusNode = null;
+            sel.anchorOffset = sel.focusOffset = 0;
+            sel.rangeCount = 0;
+            sel.isCollapsed = true;
+            sel._ranges.length = 0;
+        }
+
+        function getNativeRange(range) {
+            var nativeRange;
+            if (range instanceof DomRange) {
+                nativeRange = api.createNativeRange(range.getDocument());
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+            } else if (range instanceof WrappedRange) {
+                nativeRange = range.nativeRange;
+            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+                nativeRange = range;
+            }
+            return nativeRange;
+        }
+
+        function rangeContainsSingleElement(rangeNodes) {
+            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+                return false;
+            }
+            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function getSingleElementFromRange(range) {
+            var nodes = range.getNodes();
+            if (!rangeContainsSingleElement(nodes)) {
+                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+            }
+            return nodes[0];
+        }
+
+        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+        function isTextRange(range) {
+            return !!range && typeof range.text != "undefined";
+        }
+
+        function updateFromTextRange(sel, range) {
+            // Create a Range from the selected TextRange
+            var wrappedRange = new WrappedRange(range);
+            sel._ranges = [wrappedRange];
+
+            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+            sel.rangeCount = 1;
+            sel.isCollapsed = wrappedRange.collapsed;
+        }
+
+        function updateControlSelection(sel) {
+            // Update the wrapped selection based on what's now in the native selection
+            sel._ranges.length = 0;
+            if (sel.docSelection.type == "None") {
+                updateEmptySelection(sel);
+            } else {
+                var controlRange = sel.docSelection.createRange();
+                if (isTextRange(controlRange)) {
+                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                    // ControlRange have been removed from the ControlRange and removed from the document.
+                    updateFromTextRange(sel, controlRange);
+                } else {
+                    sel.rangeCount = controlRange.length;
+                    var range, doc = getDocument(controlRange.item(0));
+                    for (var i = 0; i < sel.rangeCount; ++i) {
+                        range = api.createRange(doc);
+                        range.selectNode(controlRange.item(i));
+                        sel._ranges.push(range);
+                    }
+                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+                }
+            }
+        }
+
+        function addRangeToControlSelection(sel, range) {
+            var controlRange = sel.docSelection.createRange();
+            var rangeElement = getSingleElementFromRange(range);
+
+            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+            // contained by the supplied range
+            var doc = getDocument(controlRange.item(0));
+            var newControlRange = getBody(doc).createControlRange();
+            for (var i = 0, len = controlRange.length; i < len; ++i) {
+                newControlRange.add(controlRange.item(i));
+            }
+            try {
+                newControlRange.add(rangeElement);
+            } catch (ex) {
+                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+            }
+            newControlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        var getSelectionRangeAt;
+
+        if (isHostMethod(testSelection, "getRangeAt")) {
+            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
+            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
+            // lesson to us all, especially me.
+            getSelectionRangeAt = function(sel, index) {
+                try {
+                    return sel.getRangeAt(index);
+                } catch (ex) {
+                    return null;
+                }
+            };
+        } else if (selectionHasAnchorAndFocus) {
+            getSelectionRangeAt = function(sel) {
+                var doc = getDocument(sel.anchorNode);
+                var range = api.createRange(doc);
+                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
+
+                // Handle the case when the selection was selected backwards (from the end to the start in the
+                // document)
+                if (range.collapsed !== this.isCollapsed) {
+                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
+                }
+
+                return range;
+            };
+        }
+
+        function WrappedSelection(selection, docSelection, win) {
+            this.nativeSelection = selection;
+            this.docSelection = docSelection;
+            this._ranges = [];
+            this.win = win;
+            this.refresh();
+        }
+
+        WrappedSelection.prototype = api.selectionPrototype;
+
+        function deleteProperties(sel) {
+            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
+            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
+            sel.detached = true;
+        }
+
+        var cachedRangySelections = [];
+
+        function actOnCachedSelection(win, action) {
+            var i = cachedRangySelections.length, cached, sel;
+            while (i--) {
+                cached = cachedRangySelections[i];
+                sel = cached.selection;
+                if (action == "deleteAll") {
+                    deleteProperties(sel);
+                } else if (cached.win == win) {
+                    if (action == "delete") {
+                        cachedRangySelections.splice(i, 1);
+                        return true;
+                    } else {
+                        return sel;
+                    }
+                }
+            }
+            if (action == "deleteAll") {
+                cachedRangySelections.length = 0;
+            }
+            return null;
+        }
+
+        var getSelection = function(win) {
+            // Check if the parameter is a Rangy Selection object
+            if (win && win instanceof WrappedSelection) {
+                win.refresh();
+                return win;
+            }
+
+            win = getWindow(win, "getNativeSelection");
+
+            var sel = actOnCachedSelection(win);
+            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+            if (sel) {
+                sel.nativeSelection = nativeSel;
+                sel.docSelection = docSel;
+                sel.refresh();
+            } else {
+                sel = new WrappedSelection(nativeSel, docSel, win);
+                cachedRangySelections.push( { win: win, selection: sel } );
+            }
+            return sel;
+        };
+
+        api.getSelection = getSelection;
+
+        api.getIframeSelection = function(iframeEl) {
+            module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
+            return api.getSelection(dom.getIframeWindow(iframeEl));
+        };
+
+        var selProto = WrappedSelection.prototype;
+
+        function createControlSelection(sel, ranges) {
+            // Ensure that the selection becomes of type "Control"
+            var doc = getDocument(ranges[0].startContainer);
+            var controlRange = getBody(doc).createControlRange();
+            for (var i = 0, el, len = ranges.length; i < len; ++i) {
+                el = getSingleElementFromRange(ranges[i]);
+                try {
+                    controlRange.add(el);
+                } catch (ex) {
+                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
+                }
+            }
+            controlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        // Selecting a range
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+            selProto.removeAllRanges = function() {
+                this.nativeSelection.removeAllRanges();
+                updateEmptySelection(this);
+            };
+
+            var addRangeBackward = function(sel, range) {
+                addRangeBackwardToNative(sel.nativeSelection, range);
+                sel.refresh();
+            };
+
+            if (selectionHasRangeCount) {
+                selProto.addRange = function(range, direction) {
+                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                        addRangeToControlSelection(this, range);
+                    } else {
+                        if (isDirectionBackward(direction) && selectionHasExtend) {
+                            addRangeBackward(this, range);
+                        } else {
+                            var previousRangeCount;
+                            if (selectionSupportsMultipleRanges) {
+                                previousRangeCount = this.rangeCount;
+                            } else {
+                                this.removeAllRanges();
+                                previousRangeCount = 0;
+                            }
+                            // Clone the native range so that changing the selected range does not affect the selection.
+                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
+                            // issue 80.
+                            var clonedNativeRange = getNativeRange(range).cloneRange();
+                            try {
+                                this.nativeSelection.addRange(clonedNativeRange);
+                            } catch (ex) {
+                            }
+
+                            // Check whether adding the range was successful
+                            this.rangeCount = this.nativeSelection.rangeCount;
+
+                            if (this.rangeCount == previousRangeCount + 1) {
+                                // The range was added successfully
+
+                                // Check whether the range that we added to the selection is reflected in the last range extracted from
+                                // the selection
+                                if (api.config.checkSelectionRanges) {
+                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
+                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                        range = new WrappedRange(nativeRange);
+                                    }
+                                }
+                                this._ranges[this.rangeCount - 1] = range;
+                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
+                                this.isCollapsed = selectionIsCollapsed(this);
+                            } else {
+                                // The range was not added successfully. The simplest thing is to refresh
+                                this.refresh();
+                            }
+                        }
+                    }
+                };
+            } else {
+                selProto.addRange = function(range, direction) {
+                    if (isDirectionBackward(direction) && selectionHasExtend) {
+                        addRangeBackward(this, range);
+                    } else {
+                        this.nativeSelection.addRange(getNativeRange(range));
+                        this.refresh();
+                    }
+                };
+            }
+
+            selProto.setRanges = function(ranges) {
+                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
+                    createControlSelection(this, ranges);
+                } else {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        this.addRange(ranges[i]);
+                    }
+                }
+            };
+        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
+                   implementsControlRange && useDocumentSelection) {
+
+            selProto.removeAllRanges = function() {
+                // Added try/catch as fix for issue #21
+                try {
+                    this.docSelection.empty();
+
+                    // Check for empty() not working (issue #24)
+                    if (this.docSelection.type != "None") {
+                        // Work around failure to empty a control selection by instead selecting a TextRange and then
+                        // calling empty()
+                        var doc;
+                        if (this.anchorNode) {
+                            doc = getDocument(this.anchorNode);
+                        } else if (this.docSelection.type == CONTROL) {
+                            var controlRange = this.docSelection.createRange();
+                            if (controlRange.length) {
+                                doc = getDocument( controlRange.item(0) );
+                            }
+                        }
+                        if (doc) {
+                            var textRange = getBody(doc).createTextRange();
+                            textRange.select();
+                            this.docSelection.empty();
+                        }
+                    }
+                } catch(ex) {}
+                updateEmptySelection(this);
+            };
+
+            selProto.addRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    api.WrappedTextRange.rangeToTextRange(range).select();
+                    this._ranges[0] = range;
+                    this.rangeCount = 1;
+                    this.isCollapsed = this._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(this, range, false);
+                }
+            };
+
+            selProto.setRanges = function(ranges) {
+                this.removeAllRanges();
+                var rangeCount = ranges.length;
+                if (rangeCount > 1) {
+                    createControlSelection(this, ranges);
+                } else if (rangeCount) {
+                    this.addRange(ranges[0]);
+                }
+            };
+        } else {
+            module.fail("No means of selecting a Range or TextRange was found");
+            return false;
+        }
+
+        selProto.getRangeAt = function(index) {
+            if (index < 0 || index >= this.rangeCount) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            } else {
+                // Clone the range to preserve selection-range independence. See issue 80.
+                return this._ranges[index].cloneRange();
+            }
+        };
+
+        var refreshSelection;
+
+        if (useDocumentSelection) {
+            refreshSelection = function(sel) {
+                var range;
+                if (api.isSelectionValid(sel.win)) {
+                    range = sel.docSelection.createRange();
+                } else {
+                    range = getBody(sel.win.document).createTextRange();
+                    range.collapse(true);
+                }
+
+                if (sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else if (isTextRange(range)) {
+                    updateFromTextRange(sel, range);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
+            refreshSelection = function(sel) {
+                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else {
+                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                    if (sel.rangeCount) {
+                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                        }
+                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
+                        sel.isCollapsed = selectionIsCollapsed(sel);
+                    } else {
+                        updateEmptySelection(sel);
+                    }
+                }
+            };
+        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
+            refreshSelection = function(sel) {
+                var range, nativeSel = sel.nativeSelection;
+                if (nativeSel.anchorNode) {
+                    range = getSelectionRangeAt(nativeSel, 0);
+                    sel._ranges = [range];
+                    sel.rangeCount = 1;
+                    updateAnchorAndFocusFromNativeSelection(sel);
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else {
+            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+            return false;
+        }
+
+        selProto.refresh = function(checkForChanges) {
+            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+
+            refreshSelection(this);
+            if (checkForChanges) {
+                // Check the range count first
+                var i = oldRanges.length;
+                if (i != this._ranges.length) {
+                    return true;
+                }
+
+                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
+                // ranges after this
+                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
+                    return true;
+                }
+
+                // Finally, compare each range in turn
+                while (i--) {
+                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        };
+
+        // Removal of a single range
+        var removeRangeManually = function(sel, range) {
+            var ranges = sel.getAllRanges();
+            sel.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                if (!rangesEqual(range, ranges[i])) {
+                    sel.addRange(ranges[i]);
+                }
+            }
+            if (!sel.rangeCount) {
+                updateEmptySelection(sel);
+            }
+        };
+
+        if (implementsControlRange && implementsDocSelection) {
+            selProto.removeRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    var controlRange = this.docSelection.createRange();
+                    var rangeElement = getSingleElementFromRange(range);
+
+                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                    // element contained by the supplied range
+                    var doc = getDocument(controlRange.item(0));
+                    var newControlRange = getBody(doc).createControlRange();
+                    var el, removed = false;
+                    for (var i = 0, len = controlRange.length; i < len; ++i) {
+                        el = controlRange.item(i);
+                        if (el !== rangeElement || removed) {
+                            newControlRange.add(controlRange.item(i));
+                        } else {
+                            removed = true;
+                        }
+                    }
+                    newControlRange.select();
+
+                    // Update the wrapped selection based on what's now in the native selection
+                    updateControlSelection(this);
+                } else {
+                    removeRangeManually(this, range);
+                }
+            };
+        } else {
+            selProto.removeRange = function(range) {
+                removeRangeManually(this, range);
+            };
+        }
+
+        // Detecting if a selection is backward
+        var selectionIsBackward;
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
+            selectionIsBackward = winSelectionIsBackward;
+
+            selProto.isBackward = function() {
+                return selectionIsBackward(this);
+            };
+        } else {
+            selectionIsBackward = selProto.isBackward = function() {
+                return false;
+            };
+        }
+
+        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
+        selProto.isBackwards = selProto.isBackward;
+
+        // Selection stringifier
+        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
+        // The current spec does not yet define this method.
+        selProto.toString = function() {
+            var rangeTexts = [];
+            for (var i = 0, len = this.rangeCount; i < len; ++i) {
+                rangeTexts[i] = "" + this._ranges[i];
+            }
+            return rangeTexts.join("");
+        };
+
+        function assertNodeInSameDocument(sel, node) {
+            if (sel.win.document != getDocument(node)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+        }
+
+        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
+        selProto.collapse = function(node, offset) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.collapseToPoint(node, offset);
+            this.setSingleRange(range);
+            this.isCollapsed = true;
+        };
+
+        selProto.collapseToStart = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[0];
+                this.collapse(range.startContainer, range.startOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        selProto.collapseToEnd = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[this.rangeCount - 1];
+                this.collapse(range.endContainer, range.endOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
+        // never used by Rangy.
+        selProto.selectAllChildren = function(node) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.selectNodeContents(node);
+            this.setSingleRange(range);
+        };
+
+        selProto.deleteFromDocument = function() {
+            // Sepcial behaviour required for IE's control selections
+            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var element;
+                while (controlRange.length) {
+                    element = controlRange.item(0);
+                    controlRange.remove(element);
+                    element.parentNode.removeChild(element);
+                }
+                this.refresh();
+            } else if (this.rangeCount) {
+                var ranges = this.getAllRanges();
+                if (ranges.length) {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        ranges[i].deleteContents();
+                    }
+                    // The spec says nothing about what the selection should contain after calling deleteContents on each
+                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
+                    this.addRange(ranges[len - 1]);
+                }
+            }
+        };
+
+        // The following are non-standard extensions
+        selProto.eachRange = function(func, returnValue) {
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                if ( func( this.getRangeAt(i) ) ) {
+                    return returnValue;
+                }
+            }
+        };
+
+        selProto.getAllRanges = function() {
+            var ranges = [];
+            this.eachRange(function(range) {
+                ranges.push(range);
+            });
+            return ranges;
+        };
+
+        selProto.setSingleRange = function(range, direction) {
+            this.removeAllRanges();
+            this.addRange(range, direction);
+        };
+
+        selProto.callMethodOnEachRange = function(methodName, params) {
+            var results = [];
+            this.eachRange( function(range) {
+                results.push( range[methodName].apply(range, params) );
+            } );
+            return results;
+        };
+        
+        function createStartOrEndSetter(isStart) {
+            return function(node, offset) {
+                var range;
+                if (this.rangeCount) {
+                    range = this.getRangeAt(0);
+                    range["set" + (isStart ? "Start" : "End")](node, offset);
+                } else {
+                    range = api.createRange(this.win.document);
+                    range.setStartAndEnd(node, offset);
+                }
+                this.setSingleRange(range, this.isBackward());
+            };
+        }
+
+        selProto.setStart = createStartOrEndSetter(true);
+        selProto.setEnd = createStartOrEndSetter(false);
+        
+        // Add select() method to Range prototype. Any existing selection will be removed.
+        api.rangePrototype.select = function(direction) {
+            getSelection( this.getDocument() ).setSingleRange(this, direction);
+        };
+
+        selProto.changeEachRange = function(func) {
+            var ranges = [];
+            var backward = this.isBackward();
+
+            this.eachRange(function(range) {
+                func(range);
+                ranges.push(range);
+            });
+
+            this.removeAllRanges();
+            if (backward && ranges.length == 1) {
+                this.addRange(ranges[0], "backward");
+            } else {
+                this.setRanges(ranges);
+            }
+        };
+
+        selProto.containsNode = function(node, allowPartial) {
+            return this.eachRange( function(range) {
+                return range.containsNode(node, allowPartial);
+            }, true ) || false;
+        };
+
+        selProto.getBookmark = function(containerNode) {
+            return {
+                backward: this.isBackward(),
+                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
+            };
+        };
+
+        selProto.moveToBookmark = function(bookmark) {
+            var selRanges = [];
+            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
+                range = api.createRange(this.win);
+                range.moveToBookmark(rangeBookmark);
+                selRanges.push(range);
+            }
+            if (bookmark.backward) {
+                this.setSingleRange(selRanges[0], "backward");
+            } else {
+                this.setRanges(selRanges);
+            }
+        };
+
+        selProto.toHtml = function() {
+            var rangeHtmls = [];
+            this.eachRange(function(range) {
+                rangeHtmls.push( DomRange.toHtml(range) );
+            });
+            return rangeHtmls.join("");
+        };
+
+        if (features.implementsTextRange) {
+            selProto.getNativeTextRange = function() {
+                var sel, textRange;
+                if ( (sel = this.docSelection) ) {
+                    var range = sel.createRange();
+                    if (isTextRange(range)) {
+                        return range;
+                    } else {
+                        throw module.createError("getNativeTextRange: selection is a control selection"); 
+                    }
+                } else if (this.rangeCount > 0) {
+                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
+                } else {
+                    throw module.createError("getNativeTextRange: selection contains no range");
+                }
+            };
+        }
+
+        function inspect(sel) {
+            var rangeInspects = [];
+            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+            if (typeof sel.rangeCount != "undefined") {
+                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+                }
+            }
+            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+        }
+
+        selProto.getName = function() {
+            return "WrappedSelection";
+        };
+
+        selProto.inspect = function() {
+            return inspect(this);
+        };
+
+        selProto.detach = function() {
+            actOnCachedSelection(this.win, "delete");
+            deleteProperties(this);
+        };
+
+        WrappedSelection.detachAll = function() {
+            actOnCachedSelection(null, "deleteAll");
+        };
+
+        WrappedSelection.inspect = inspect;
+        WrappedSelection.isDirectionBackward = isDirectionBackward;
+
+        api.Selection = WrappedSelection;
+
+        api.selectionPrototype = selProto;
+
+        api.addShimListener(function(win) {
+            if (typeof win.getSelection == "undefined") {
+                win.getSelection = function() {
+                    return getSelection(win);
+                };
+            }
+            win = null;
+        });
+    });
+    
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Wait for document to load before initializing
+    var docReady = false;
+
+    var loadHandler = function(e) {
+        if (!docReady) {
+            docReady = true;
+            if (!api.initialized && api.config.autoInitialize) {
+                init();
+            }
+        }
+    };
+
+    if (isBrowser) {
+        // Test whether the document has already been loaded and initialize immediately if so
+        if (document.readyState == "complete") {
+            loadHandler();
+        } else {
+            if (isHostMethod(document, "addEventListener")) {
+                document.addEventListener("DOMContentLoaded", loadHandler, false);
+            }
+
+            // Add a fallback in case the DOMContentLoaded event isn't supported
+            addListener(window, "load", loadHandler);
+        }
+    }
+
+    return api;
+}, this);;/**
+ * Selection save and restore module for Rangy.
+ * Saves and restores user selections using marker invisible elements in the DOM.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2014, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0-alpha.20140921
+ * Build date: 21 September 2014
+ */
+(function(factory, root) {
+    if (typeof define == "function" && define.amd) {
+        // AMD. Register as an anonymous module with a dependency on Rangy.
+        define(["./rangy-core"], factory);
+    } else if (typeof module != "undefined" && typeof exports == "object") {
+        // Node/CommonJS style
+        module.exports = factory( require("rangy") );
+    } else {
+        // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
+        factory(root.rangy);
+    }
+})(function(rangy) {
+    rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
+        var dom = api.dom;
+
+        var markerTextChar = "\ufeff";
+
+        function gEBI(id, doc) {
+            return (doc || document).getElementById(id);
+        }
+
+        function insertRangeBoundaryMarker(range, atStart) {
+            var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
+            var markerEl;
+            var doc = dom.getDocument(range.startContainer);
+
+            // Clone the Range and collapse to the appropriate boundary point
+            var boundaryRange = range.cloneRange();
+            boundaryRange.collapse(atStart);
+
+            // Create the marker element containing a single invisible character using DOM methods and insert it
+            markerEl = doc.createElement("span");
+            markerEl.id = markerId;
+            markerEl.style.lineHeight = "0";
+            markerEl.style.display = "none";
+            markerEl.className = "rangySelectionBoundary";
+            markerEl.appendChild(doc.createTextNode(markerTextChar));
+
+            boundaryRange.insertNode(markerEl);
+            return markerEl;
+        }
+
+        function setRangeBoundary(doc, range, markerId, atStart) {
+            var markerEl = gEBI(markerId, doc);
+            if (markerEl) {
+                range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
+                markerEl.parentNode.removeChild(markerEl);
+            } else {
+                module.warn("Marker element has been removed. Cannot restore selection.");
+            }
+        }
+
+        function compareRanges(r1, r2) {
+            return r2.compareBoundaryPoints(r1.START_TO_START, r1);
+        }
+
+        function saveRange(range, backward) {
+            var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
+
+            if (range.collapsed) {
+                endEl = insertRangeBoundaryMarker(range, false);
+                return {
+                    document: doc,
+                    markerId: endEl.id,
+                    collapsed: true
+                };
+            } else {
+                endEl = insertRangeBoundaryMarker(range, false);
+                startEl = insertRangeBoundaryMarker(range, true);
+
+                return {
+                    document: doc,
+                    startMarkerId: startEl.id,
+                    endMarkerId: endEl.id,
+                    collapsed: false,
+                    backward: backward,
+                    toString: function() {
+                        return "original text: '" + text + "', new text: '" + range.toString() + "'";
+                    }
+                };
+            }
+        }
+
+        function restoreRange(rangeInfo, normalize) {
+            var doc = rangeInfo.document;
+            if (typeof normalize == "undefined") {
+                normalize = true;
+            }
+            var range = api.createRange(doc);
+            if (rangeInfo.collapsed) {
+                var markerEl = gEBI(rangeInfo.markerId, doc);
+                if (markerEl) {
+                    markerEl.style.display = "inline";
+                    var previousNode = markerEl.previousSibling;
+
+                    // Workaround for issue 17
+                    if (previousNode && previousNode.nodeType == 3) {
+                        markerEl.parentNode.removeChild(markerEl);
+                        range.collapseToPoint(previousNode, previousNode.length);
+                    } else {
+                        range.collapseBefore(markerEl);
+                        markerEl.parentNode.removeChild(markerEl);
+                    }
+                } else {
+                    module.warn("Marker element has been removed. Cannot restore selection.");
+                }
+            } else {
+                setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
+                setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
+            }
+
+            if (normalize) {
+                range.normalizeBoundaries();
+            }
+
+            return range;
+        }
+
+        function saveRanges(ranges, backward) {
+            var rangeInfos = [], range, doc;
+
+            // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
+            ranges = ranges.slice(0);
+            ranges.sort(compareRanges);
+
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                rangeInfos[i] = saveRange(ranges[i], backward);
+            }
+
+            // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
+            // between its markers
+            for (i = len - 1; i >= 0; --i) {
+                range = ranges[i];
+                doc = api.DomRange.getRangeDocument(range);
+                if (range.collapsed) {
+                    range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
+                } else {
+                    range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
+                    range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
+                }
+            }
+
+            return rangeInfos;
+        }
+
+        function saveSelection(win) {
+            if (!api.isSelectionValid(win)) {
+                module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
+                return null;
+            }
+            var sel = api.getSelection(win);
+            var ranges = sel.getAllRanges();
+            var backward = (ranges.length == 1 && sel.isBackward());
+
+            var rangeInfos = saveRanges(ranges, backward);
+
+            // Ensure current selection is unaffected
+            if (backward) {
+                sel.setSingleRange(ranges[0], "backward");
+            } else {
+                sel.setRanges(ranges);
+            }
+
+            return {
+                win: win,
+                rangeInfos: rangeInfos,
+                restored: false
+            };
+        }
+
+        function restoreRanges(rangeInfos) {
+            var ranges = [];
+
+            // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
+            // normalization affecting previously restored ranges.
+            var rangeCount = rangeInfos.length;
+
+            for (var i = rangeCount - 1; i >= 0; i--) {
+                ranges[i] = restoreRange(rangeInfos[i], true);
+            }
+
+            return ranges;
+        }
+
+        function restoreSelection(savedSelection, preserveDirection) {
+            if (!savedSelection.restored) {
+                var rangeInfos = savedSelection.rangeInfos;
+                var sel = api.getSelection(savedSelection.win);
+                var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
+
+                if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
+                    sel.removeAllRanges();
+                    sel.addRange(ranges[0], true);
+                } else {
+                    sel.setRanges(ranges);
+                }
+
+                savedSelection.restored = true;
+            }
+        }
+
+        function removeMarkerElement(doc, markerId) {
+            var markerEl = gEBI(markerId, doc);
+            if (markerEl) {
+                markerEl.parentNode.removeChild(markerEl);
+            }
+        }
+
+        function removeMarkers(savedSelection) {
+            var rangeInfos = savedSelection.rangeInfos;
+            for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
+                rangeInfo = rangeInfos[i];
+                if (rangeInfo.collapsed) {
+                    removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
+                } else {
+                    removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
+                    removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
+                }
+            }
+        }
+
+        api.util.extend(api, {
+            saveRange: saveRange,
+            restoreRange: restoreRange,
+            saveRanges: saveRanges,
+            restoreRanges: restoreRanges,
+            saveSelection: saveSelection,
+            restoreSelection: restoreSelection,
+            removeMarkerElement: removeMarkerElement,
+            removeMarkers: removeMarkers
+        });
+    });
+    
+}, this);;/*
+	Base.js, version 1.1a
+	Copyright 2006-2010, Dean Edwards
+	License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+var Base = function() {
+	// dummy
+};
+
+Base.extend = function(_instance, _static) { // subclass
+	var extend = Base.prototype.extend;
+	
+	// build the prototype
+	Base._prototyping = true;
+	var proto = new this;
+	extend.call(proto, _instance);
+  proto.base = function() {
+    // call this method from any other method to invoke that method's ancestor
+  };
+	delete Base._prototyping;
+	
+	// create the wrapper for the constructor function
+	//var constructor = proto.constructor.valueOf(); //-dean
+	var constructor = proto.constructor;
+	var klass = proto.constructor = function() {
+		if (!Base._prototyping) {
+			if (this._constructing || this.constructor == klass) { // instantiation
+				this._constructing = true;
+				constructor.apply(this, arguments);
+				delete this._constructing;
+			} else if (arguments[0] != null) { // casting
+				return (arguments[0].extend || extend).call(arguments[0], proto);
+			}
+		}
+	};
+	
+	// build the class interface
+	klass.ancestor = this;
+	klass.extend = this.extend;
+	klass.forEach = this.forEach;
+	klass.implement = this.implement;
+	klass.prototype = proto;
+	klass.toString = this.toString;
+	klass.valueOf = function(type) {
+		//return (type == "object") ? klass : constructor; //-dean
+		return (type == "object") ? klass : constructor.valueOf();
+	};
+	extend.call(klass, _static);
+	// class initialisation
+	if (typeof klass.init == "function") klass.init();
+	return klass;
+};
+
+Base.prototype = {	
+	extend: function(source, value) {
+		if (arguments.length > 1) { // extending with a name/value pair
+			var ancestor = this[source];
+			if (ancestor && (typeof value == "function") && // overriding a method?
+				// the valueOf() comparison is to avoid circular references
+				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
+				/\bbase\b/.test(value)) {
+				// get the underlying method
+				var method = value.valueOf();
+				// override
+				value = function() {
+					var previous = this.base || Base.prototype.base;
+					this.base = ancestor;
+					var returnValue = method.apply(this, arguments);
+					this.base = previous;
+					return returnValue;
+				};
+				// point to the underlying method
+				value.valueOf = function(type) {
+					return (type == "object") ? value : method;
+				};
+				value.toString = Base.toString;
+			}
+			this[source] = value;
+		} else if (source) { // extending with an object literal
+			var extend = Base.prototype.extend;
+			// if this object has a customised extend method then use it
+			if (!Base._prototyping && typeof this != "function") {
+				extend = this.extend || extend;
+			}
+			var proto = {toSource: null};
+			// do the "toString" and other methods manually
+			var hidden = ["constructor", "toString", "valueOf"];
+			// if we are prototyping then include the constructor
+			var i = Base._prototyping ? 0 : 1;
+			while (key = hidden[i++]) {
+				if (source[key] != proto[key]) {
+					extend.call(this, key, source[key]);
+
+				}
+			}
+			// copy each of the source object's properties to this object
+			for (var key in source) {
+				if (!proto[key]) extend.call(this, key, source[key]);
+			}
+		}
+		return this;
+	}
+};
+
+// initialise
+Base = Base.extend({
+	constructor: function() {
+		this.extend(arguments[0]);
+	}
+}, {
+	ancestor: Object,
+	version: "1.1",
+	
+	forEach: function(object, block, context) {
+		for (var key in object) {
+			if (this.prototype[key] === undefined) {
+				block.call(context, object[key], key, object);
+			}
+		}
+	},
+		
+	implement: function() {
+		for (var i = 0; i < arguments.length; i++) {
+			if (typeof arguments[i] == "function") {
+				// if it's a function, call it
+				arguments[i](this.prototype);
+			} else {
+				// add the interface using the extend method
+				this.prototype.extend(arguments[i]);
+			}
+		}
+		return this;
+	},
+	
+	toString: function() {
+		return String(this.valueOf());
+	}
+});;/**
+ * Detect browser support for specific features
+ */
+wysihtml5.browser = (function() {
+  var userAgent   = navigator.userAgent,
+      testElement = document.createElement("div"),
+      // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
+      isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1,
+      isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1,
+      isChrome    = userAgent.indexOf("Chrome/")      !== -1,
+      isOpera     = userAgent.indexOf("Opera/")       !== -1;
+
+  function iosVersion(userAgent) {
+    return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
+  }
+
+  function androidVersion(userAgent) {
+    return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
+  }
+
+  function isIE(version, equation) {
+    var rv = -1,
+        re;
+
+    if (navigator.appName == 'Microsoft Internet Explorer') {
+      re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+    } else if (navigator.appName == 'Netscape') {
+      re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
+    }
+
+    if (re && re.exec(navigator.userAgent) != null) {
+      rv = parseFloat(RegExp.$1);
+    }
+
+    if (rv === -1) { return false; }
+    if (!version) { return true; }
+    if (!equation) { return version === rv; }
+    if (equation === "<") { return version < rv; }
+    if (equation === ">") { return version > rv; }
+    if (equation === "<=") { return version <= rv; }
+    if (equation === ">=") { return version >= rv; }
+  }
+
+  return {
+    // Static variable needed, publicly accessible, to be able override it in unit tests
+    USER_AGENT: userAgent,
+
+    /**
+     * Exclude browsers that are not capable of displaying and handling
+     * contentEditable as desired:
+     *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
+     *    - IE < 8 create invalid markup and crash randomly from time to time
+     *
+     * @return {Boolean}
+     */
+    supported: function() {
+      var userAgent                   = this.USER_AGENT.toLowerCase(),
+          // Essential for making html elements editable
+          hasContentEditableSupport   = "contentEditable" in testElement,
+          // Following methods are needed in order to interact with the contentEditable area
+          hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
+          // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
+          hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
+          // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
+          isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
+      return hasContentEditableSupport
+        && hasEditingApiSupport
+        && hasQuerySelectorSupport
+        && !isIncompatibleMobileBrowser;
+    },
+
+    isTouchDevice: function() {
+      return this.supportsEvent("touchmove");
+    },
+
+    isIos: function() {
+      return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
+    },
+
+    isAndroid: function() {
+      return this.USER_AGENT.indexOf("Android") !== -1;
+    },
+
+    /**
+     * Whether the browser supports sandboxed iframes
+     * Currently only IE 6+ offers such feature 
+ * ... becomes ... + *
hello
+ * + *
hello
+ * ... becomes ... + *
hello
+ */ +var wysihtml5ParserRules = { + /** + * CSS Class white-list + * Following CSS classes won't be removed when parsed by the wysihtml5 HTML parser + */ + "classes": { + "wysiwyg-clear-both": 1, + "wysiwyg-clear-left": 1, + "wysiwyg-clear-right": 1, + "wysiwyg-color-aqua": 1, + "wysiwyg-color-black": 1, + "wysiwyg-color-blue": 1, + "wysiwyg-color-fuchsia": 1, + "wysiwyg-color-gray": 1, + "wysiwyg-color-green": 1, + "wysiwyg-color-lime": 1, + "wysiwyg-color-maroon": 1, + "wysiwyg-color-navy": 1, + "wysiwyg-color-olive": 1, + "wysiwyg-color-purple": 1, + "wysiwyg-color-red": 1, + "wysiwyg-color-silver": 1, + "wysiwyg-color-teal": 1, + "wysiwyg-color-white": 1, + "wysiwyg-color-yellow": 1, + "wysiwyg-float-left": 1, + "wysiwyg-float-right": 1, + "wysiwyg-font-size-large": 1, + "wysiwyg-font-size-larger": 1, + "wysiwyg-font-size-medium": 1, + "wysiwyg-font-size-small": 1, + "wysiwyg-font-size-smaller": 1, + "wysiwyg-font-size-x-large": 1, + "wysiwyg-font-size-x-small": 1, + "wysiwyg-font-size-xx-large": 1, + "wysiwyg-font-size-xx-small": 1, + "wysiwyg-text-align-center": 1, + "wysiwyg-text-align-justify": 1, + "wysiwyg-text-align-left": 1, + "wysiwyg-text-align-right": 1 + }, + /** + * Tag list + * + * The following options are available: + * + * - add_class: converts and deletes the given HTML4 attribute (align, clear, ...) via the given method to a css class + * The following methods are implemented in wysihtml5.dom.parse: + * - align_text: converts align attribute values (right/left/center/justify) to their corresponding css class "wysiwyg-text-align-*") + *

foo

... becomes ...

foo

+ * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*" + *
... becomes ...
+ * - align_img: converts align attribute values (right/left) on to their corresponding css class "wysiwyg-float-*" + * + * - add_style: converts and deletes the given HTML4 attribute (align) via the given method to a css style + * The following methods are implemented in wysihtml5.dom.parse: + * - align_text: converts align attribute values (right/left/center) to their corresponding css style) + *

foo

... becomes ...

foo

+ * + * - remove: removes the element and its content + * + * - unwrap removes element but leaves content + * + * - rename_tag: renames the element to the given tag + * + * - set_class: adds the given class to the element (note: make sure that the class is in the "classes" white list above) + * + * - set_attributes: sets/overrides the given attributes + * + * - check_attributes: checks the given HTML attribute via the given method + * - url: allows only valid urls (starting with http:// or https://) + * - src: allows something like "/foobar.jpg", "http://google.com", ... + * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg" + * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML) + * - numbers: ensures that the attribute only contains numeric characters + * - any: allows anything to pass + */ + "tags": { + "tr": { + "add_class": { + "align": "align_text" + } + }, + "strike": { + "remove": 1 + }, + "form": { + "rename_tag": "div" + }, + "rt": { + "rename_tag": "span" + }, + "code": {}, + "acronym": { + "rename_tag": "span" + }, + "br": { + "add_class": { + "clear": "clear_br" + } + }, + "details": { + "rename_tag": "div" + }, + "h4": { + "add_class": { + "align": "align_text" + } + }, + "em": {}, + "title": { + "remove": 1 + }, + "multicol": { + "rename_tag": "div" + }, + "figure": { + "rename_tag": "div" + }, + "xmp": { + "rename_tag": "span" + }, + "small": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-smaller" + }, + "area": { + "remove": 1 + }, + "time": { + "rename_tag": "span" + }, + "dir": { + "rename_tag": "ul" + }, + "bdi": { + "rename_tag": "span" + }, + "command": { + "remove": 1 + }, + "ul": {}, + "progress": { + "rename_tag": "span" + }, + "dfn": { + "rename_tag": "span" + }, + "iframe": { + "remove": 1 + }, + "figcaption": { + "rename_tag": "div" + }, + "a": { + "check_attributes": { + "target": "any", + "href": "url" // if you compiled master manually then change this from 'url' to 'href' + }, + "set_attributes": { + "rel": "nofollow" + } + }, + "img": { + "check_attributes": { + "width": "numbers", + "alt": "alt", + "src": "url", // if you compiled master manually then change this from 'url' to 'src' + "height": "numbers" + }, + "add_class": { + "align": "align_img" + } + }, + "rb": { + "rename_tag": "span" + }, + "footer": { + "rename_tag": "div" + }, + "noframes": { + "remove": 1 + }, + "abbr": { + "rename_tag": "span" + }, + "u": {}, + "bgsound": { + "remove": 1 + }, + "sup": { + "rename_tag": "span" + }, + "address": { + "rename_tag": "div" + }, + "basefont": { + "remove": 1 + }, + "nav": { + "rename_tag": "div" + }, + "h1": { + "add_class": { + "align": "align_text" + } + }, + "head": { + "remove": 1 + }, + "tbody": { + "add_class": { + "align": "align_text" + } + }, + "dd": { + "rename_tag": "div" + }, + "s": { + "rename_tag": "span" + }, + "li": {}, + "td": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers" + }, + "add_class": { + "align": "align_text" + } + }, + "object": { + "remove": 1 + }, + "div": { + "add_class": { + "align": "align_text" + } + }, + "option": { + "rename_tag": "span" + }, + "select": { + "rename_tag": "span" + }, + "i": {}, + "track": { + "remove": 1 + }, + "wbr": { + "remove": 1 + }, + "fieldset": { + "rename_tag": "div" + }, + "big": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-larger" + }, + "button": { + "rename_tag": "span" + }, + "noscript": { + "remove": 1 + }, + "svg": { + "remove": 1 + }, + "input": { + "remove": 1 + }, + "table": {}, + "keygen": { + "remove": 1 + }, + "h5": { + "add_class": { + "align": "align_text" + } + }, + "meta": { + "remove": 1 + }, + "map": { + "rename_tag": "div" + }, + "isindex": { + "remove": 1 + }, + "mark": { + "rename_tag": "span" + }, + "caption": { + "add_class": { + "align": "align_text" + } + }, + "tfoot": { + "add_class": { + "align": "align_text" + } + }, + "base": { + "remove": 1 + }, + "video": { + "remove": 1 + }, + "strong": {}, + "canvas": { + "remove": 1 + }, + "output": { + "rename_tag": "span" + }, + "marquee": { + "rename_tag": "span" + }, + "b": {}, + "q": { + "check_attributes": { + "cite": "url" + } + }, + "applet": { + "remove": 1 + }, + "span": {}, + "rp": { + "rename_tag": "span" + }, + "spacer": { + "remove": 1 + }, + "source": { + "remove": 1 + }, + "aside": { + "rename_tag": "div" + }, + "frame": { + "remove": 1 + }, + "section": { + "rename_tag": "div" + }, + "body": { + "rename_tag": "div" + }, + "ol": {}, + "nobr": { + "rename_tag": "span" + }, + "html": { + "rename_tag": "div" + }, + "summary": { + "rename_tag": "span" + }, + "var": { + "rename_tag": "span" + }, + "del": { + "remove": 1 + }, + "blockquote": { + "check_attributes": { + "cite": "url" + } + }, + "style": { + "remove": 1 + }, + "device": { + "remove": 1 + }, + "meter": { + "rename_tag": "span" + }, + "h3": { + "add_class": { + "align": "align_text" + } + }, + "textarea": { + "rename_tag": "span" + }, + "embed": { + "remove": 1 + }, + "hgroup": { + "rename_tag": "div" + }, + "font": { + "rename_tag": "span", + "add_class": { + "size": "size_font" + } + }, + "tt": { + "rename_tag": "span" + }, + "noembed": { + "remove": 1 + }, + "thead": { + "add_class": { + "align": "align_text" + } + }, + "blink": { + "rename_tag": "span" + }, + "plaintext": { + "rename_tag": "span" + }, + "xml": { + "remove": 1 + }, + "h6": { + "add_class": { + "align": "align_text" + } + }, + "param": { + "remove": 1 + }, + "th": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers" + }, + "add_class": { + "align": "align_text" + } + }, + "legend": { + "rename_tag": "span" + }, + "hr": {}, + "label": { + "rename_tag": "span" + }, + "dl": { + "rename_tag": "div" + }, + "kbd": { + "rename_tag": "span" + }, + "listing": { + "rename_tag": "div" + }, + "dt": { + "rename_tag": "span" + }, + "nextid": { + "remove": 1 + }, + "pre": {}, + "center": { + "rename_tag": "div", + "set_class": "wysiwyg-text-align-center" + }, + "audio": { + "remove": 1 + }, + "datalist": { + "rename_tag": "span" + }, + "samp": { + "rename_tag": "span" + }, + "col": { + "remove": 1 + }, + "article": { + "rename_tag": "div" + }, + "cite": {}, + "link": { + "remove": 1 + }, + "script": { + "remove": 1 + }, + "bdo": { + "rename_tag": "span" + }, + "menu": { + "rename_tag": "ul" + }, + "colgroup": { + "remove": 1 + }, + "ruby": { + "rename_tag": "span" + }, + "h2": { + "add_class": { + "align": "align_text" + } + }, + "ins": { + "rename_tag": "span" + }, + "p": { + "add_class": { + "align": "align_text" + } + }, + "sub": { + "rename_tag": "span" + }, + "comment": { + "remove": 1 + }, + "frameset": { + "remove": 1 + }, + "optgroup": { + "rename_tag": "span" + }, + "header": { + "rename_tag": "div" + } + } +}; diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_and_extended.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_and_extended.js new file mode 100644 index 0000000..84c70b4 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_and_extended.js @@ -0,0 +1,683 @@ +/** + * Full HTML5 compatibility rule set + * Loosened and extended ruleset. Allows more freedom on user side + * These rules define which tags and CSS classes are supported and which tags should be specially treated. + */ + +var wysihtml5ParserRulesDefaults = { + "blockLevelEl": { + "keep_styles": { + "textAlign": /^((left)|(right)|(center)|(justify))$/i, + "float": 1 + }, + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "id": "any" + } + }, + + "makeDiv": { + "rename_tag": "div", + "one_of_type": { + "alignment_object": 1 + }, + "remove_action": "unwrap", + "keep_styles": { + "textAlign": 1, + "float": 1 + }, + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "id": "any" + } + } +}; + +var wysihtml5ParserRules = { + /** + * CSS Class white-list + * Following CSS classes won't be removed when parsed by the wysihtml5 HTML parser + * If all classes should pass "any" as classes value. Ex: "classes": "any" + */ + "classes": "any", + + /* blacklist of classes is only available if classes is set to any */ + "classes_blacklist": { + "Apple-interchange-newline": 1, + "MsoNormal": 1, + "MsoPlainText": 1 + }, + + "type_definitions": { + + "alignment_object": { + "classes": { + "wysiwyg-text-align-center": 1, + "wysiwyg-text-align-justify": 1, + "wysiwyg-text-align-left": 1, + "wysiwyg-text-align-right": 1, + "wysiwyg-float-left": 1, + "wysiwyg-float-right": 1 + }, + "styles": { + "float": ["left", "right"], + "text-align": ["left", "right", "center"] + } + }, + + "valid_image_src": { + "attrs": { + "src": /^[^data\:]/i + } + }, + + "text_color_object": { + "styles": { + "color": true, + "background-color": true + } + }, + + "text_fontsize_object": { + "styles": { + "font-size": true + } + }, + + "text_formatting_object": { + "classes": { + "wysiwyg-color-aqua": 1, + "wysiwyg-color-black": 1, + "wysiwyg-color-blue": 1, + "wysiwyg-color-fuchsia": 1, + "wysiwyg-color-gray": 1, + "wysiwyg-color-green": 1, + "wysiwyg-color-lime": 1, + "wysiwyg-color-maroon": 1, + "wysiwyg-color-navy": 1, + "wysiwyg-color-olive": 1, + "wysiwyg-color-purple": 1, + "wysiwyg-color-red": 1, + "wysiwyg-color-silver": 1, + "wysiwyg-color-teal": 1, + "wysiwyg-color-white": 1, + "wysiwyg-color-yellow": 1, + "wysiwyg-font-size-large": 1, + "wysiwyg-font-size-larger": 1, + "wysiwyg-font-size-medium": 1, + "wysiwyg-font-size-small": 1, + "wysiwyg-font-size-smaller": 1, + "wysiwyg-font-size-x-large": 1, + "wysiwyg-font-size-x-small": 1, + "wysiwyg-font-size-xx-large": 1, + "wysiwyg-font-size-xx-small": 1 + } + } + }, + + "comments": 1, // if set allows comments to pass + + /** + * Tag list + * + * The following options are available: + * + * - add_class: converts and deletes the given HTML4 attribute (align, clear, ...) via the given method to a css class + * The following methods are implemented in wysihtml5.dom.parse: + * - align_text: converts align attribute values (right/left/center/justify) to their corresponding css class "wysiwyg-text-align-*") + *

foo

... becomes ...

class="wysiwyg-text-align-center">foo

+ * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*" + *
... becomes ...
+ * - align_img: converts align attribute values (right/left) on to their corresponding css class "wysiwyg-float-*" + * + * - remove: removes the element and its content + * + * - unwrap removes element but leaves content + * + * - rename_tag: renames the element to the given tag + * + * - set_class: adds the given class to the element (note: make sure that the class is in the "classes" white list above) + * + * - set_attributes: sets/overrides the given attributes + * + * - check_attributes: checks the given HTML attribute via the given method + * - url: allows only valid urls (starting with http:// or https://) + * - src: allows something like "/foobar.jpg", "http://google.com", ... + * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg" + * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML) + * - numbers: ensures that the attribute only contains numeric characters + * - any: allows anything to pass + */ + "tags": { + "tr": { + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "id": "any" + } + }, + "strike": { + "unwrap": 1 + }, + "form": { + "unwrap": 1 + }, + "rt": { + "rename_tag": "span" + }, + "code": {}, + "acronym": { + "rename_tag": "span" + }, + "br": { + "add_class": { + "clear": "clear_br" + } + }, + "details": { + "unwrap": 1 + }, + "h4": wysihtml5ParserRulesDefaults.blockLevelEl, + "em": {}, + "title": { + "remove": 1 + }, + "multicol": { + "unwrap": 1 + }, + "figure": { + "unwrap": 1 + }, + "xmp": { + "unwrap": 1 + }, + "small": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-smaller" + }, + "area": { + "remove": 1 + }, + "time": { + "unwrap": 1 + }, + "dir": { + "rename_tag": "ul" + }, + "bdi": { + "unwrap": 1 + }, + "command": { + "unwrap": 1 + }, + "ul": { + "check_attributes": { + "id": "any" + } + }, + "progress": { + "rename_tag": "span" + }, + "dfn": { + "unwrap": 1 + }, + "iframe": { + "check_attributes": { + "src": "any", + "width": "any", + "height": "any", + "frameborder": "any", + "style": "any", + "id": "any" + } + }, + "figcaption": { + "unwrap": 1 + }, + "a": { + "check_attributes": { + "href": "href", // if you compiled master manually then change this from 'url' to 'href' + "rel": "any", + "target": "any", + "id": "any" + } + }, + "img": { + "one_of_type": { + "valid_image_src": 1 + }, + "check_attributes": { + "width": "numbers", + "alt": "alt", + "src": "src", // if you compiled master manually then change this from 'url' to 'src' + "height": "numbers", + "id": "any" + }, + "add_class": { + "align": "align_img" + } + }, + "rb": { + "unwrap": 1 + }, + "footer": wysihtml5ParserRulesDefaults.makeDiv, + "noframes": { + "remove": 1 + }, + "abbr": { + "unwrap": 1 + }, + "u": {}, + "bgsound": { + "remove": 1 + }, + "sup": { + "unwrap": 1 + }, + "address": { + "unwrap": 1 + }, + "basefont": { + "remove": 1 + }, + "nav": { + "unwrap": 1 + }, + "h1": wysihtml5ParserRulesDefaults.blockLevelEl, + "head": { + "unwrap": 1 + }, + "tbody": wysihtml5ParserRulesDefaults.blockLevelEl, + "dd": { + "unwrap": 1 + }, + "s": { + "unwrap": 1 + }, + "li": {}, + "td": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers", + "valign": "any", + "align": "any", + "id": "any", + "class": "any" + }, + "keep_styles": { + "backgroundColor": 1, + "width": 1, + "height": 1 + }, + "add_style": { + "align": "align_text" + } + }, + "object": { + "remove": 1 + }, + + "div": { + "one_of_type": { + "alignment_object": 1 + }, + "remove_action": "unwrap", + "keep_styles": { + "textAlign": 1, + "float": 1 + }, + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "id": "any", + "contenteditable": "any" + } + }, + + "option": { + "remove":1 + }, + "select": { + "remove":1 + }, + "i": {}, + "track": { + "remove": 1 + }, + "wbr": { + "remove": 1 + }, + "fieldset": { + "unwrap": 1 + }, + "big": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-larger" + }, + "button": { + "unwrap": 1 + }, + "noscript": { + "remove": 1 + }, + "svg": { + "remove": 1 + }, + "input": { + "remove": 1 + }, + "table": { + "keep_styles": { + "width": 1, + "textAlign": 1, + "float": 1 + }, + "check_attributes": { + "id": "any" + } + }, + "keygen": { + "remove": 1 + }, + "h5": wysihtml5ParserRulesDefaults.blockLevelEl, + "meta": { + "remove": 1 + }, + "map": { + "remove": 1 + }, + "isindex": { + "remove": 1 + }, + "mark": { + "unwrap": 1 + }, + "caption": wysihtml5ParserRulesDefaults.blockLevelEl, + "tfoot": wysihtml5ParserRulesDefaults.blockLevelEl, + "base": { + "remove": 1 + }, + "video": { + "remove": 1 + }, + "strong": {}, + "canvas": { + "remove": 1 + }, + "output": { + "unwrap": 1 + }, + "marquee": { + "unwrap": 1 + }, + "b": {}, + "q": { + "check_attributes": { + "cite": "url", + "id": "any" + } + }, + "applet": { + "remove": 1 + }, + "span": { + "one_of_type": { + "text_formatting_object": 1, + "text_color_object": 1, + "text_fontsize_object": 1 + }, + "keep_styles": { + "color": 1, + "backgroundColor": 1, + "fontSize": 1 + }, + "remove_action": "unwrap", + "check_attributes": { + "id": "any" + } + }, + "rp": { + "unwrap": 1 + }, + "spacer": { + "remove": 1 + }, + "source": { + "remove": 1 + }, + "aside": wysihtml5ParserRulesDefaults.makeDiv, + "frame": { + "remove": 1 + }, + "section": wysihtml5ParserRulesDefaults.makeDiv, + "body": { + "unwrap": 1 + }, + "ol": {}, + "nobr": { + "unwrap": 1 + }, + "html": { + "unwrap": 1 + }, + "summary": { + "unwrap": 1 + }, + "var": { + "unwrap": 1 + }, + "del": { + "unwrap": 1 + }, + "blockquote": { + "keep_styles": { + "textAlign": 1, + "float": 1 + }, + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "cite": "url", + "id": "any" + } + }, + "style": { + "check_attributes": { + "type": "any", + "src": "any", + "charset": "any" + } + }, + "device": { + "remove": 1 + }, + "meter": { + "unwrap": 1 + }, + "h3": wysihtml5ParserRulesDefaults.blockLevelEl, + "textarea": { + "unwrap": 1 + }, + "embed": { + "remove": 1 + }, + "hgroup": { + "unwrap": 1 + }, + "font": { + "rename_tag": "span", + "add_class": { + "size": "size_font" + } + }, + "tt": { + "unwrap": 1 + }, + "noembed": { + "remove": 1 + }, + "thead": { + "add_style": { + "align": "align_text" + }, + "check_attributes": { + "id": "any" + } + }, + "blink": { + "unwrap": 1 + }, + "plaintext": { + "unwrap": 1 + }, + "xml": { + "remove": 1 + }, + "h6": wysihtml5ParserRulesDefaults.blockLevelEl, + "param": { + "remove": 1 + }, + "th": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers", + "valign": "any", + "align": "any", + "id": "any" + }, + "keep_styles": { + "backgroundColor": 1, + "width": 1, + "height": 1 + }, + "add_style": { + "align": "align_text" + } + }, + "legend": { + "unwrap": 1 + }, + "hr": {}, + "label": { + "unwrap": 1 + }, + "dl": { + "unwrap": 1 + }, + "kbd": { + "unwrap": 1 + }, + "listing": { + "unwrap": 1 + }, + "dt": { + "unwrap": 1 + }, + "nextid": { + "remove": 1 + }, + "pre": {}, + "center": wysihtml5ParserRulesDefaults.makeDiv, + "audio": { + "remove": 1 + }, + "datalist": { + "unwrap": 1 + }, + "samp": { + "unwrap": 1 + }, + "col": { + "remove": 1 + }, + "article": wysihtml5ParserRulesDefaults.makeDiv, + "cite": {}, + "link": { + "remove": 1 + }, + "script": { + "check_attributes": { + "type": "any", + "src": "any", + "charset": "any" + } + }, + "bdo": { + "unwrap": 1 + }, + "menu": { + "rename_tag": "ul" + }, + "colgroup": { + "remove": 1 + }, + "ruby": { + "unwrap": 1 + }, + "h2": wysihtml5ParserRulesDefaults.blockLevelEl, + "ins": { + "unwrap": 1 + }, + "p": wysihtml5ParserRulesDefaults.blockLevelEl, + "sub": { + "unwrap": 1 + }, + "comment": { + "remove": 1 + }, + "frameset": { + "remove": 1 + }, + "optgroup": { + "unwrap": 1 + }, + "header": wysihtml5ParserRulesDefaults.makeDiv + } +}; + + +(function() { + // Paste cleanup rules universal for all rules (also applied to content copied from editor) + var commonRules = wysihtml5.lang.object(wysihtml5ParserRules).clone(true); + commonRules.comments = false; + commonRules.selectors = { "a u": "unwrap"}; + commonRules.tags.style = { "remove": 1 }; + commonRules.tags.script = { "remove": 1 }; + commonRules.tags.head = { "remove": 1 }; + + // Paste cleanup for unindentified source + var universalRules = wysihtml5.lang.object(commonRules).clone(true); + universalRules.tags.div.one_of_type.alignment_object = 1; + universalRules.tags.div.remove_action = "unwrap"; + universalRules.tags.div.check_attributes.style = false; + universalRules.tags.div.keep_styles = { + "textAlign": /^((left)|(right)|(center)|(justify))$/i, + "float": 1 + }; + universalRules.tags.span.keep_styles = false; + + // Paste cleanup for MS Office + // TODO: should be extended to stricter ruleset, as current set will probably not cover all Office bizarreness + var msOfficeRules = wysihtml5.lang.object(universalRules).clone(true); + msOfficeRules.classes = {}; + + window.wysihtml5ParserPasteRulesets = [ + { + condition: //i, + set: commonRules + },{ + set: universalRules + } + ]; + +})(); diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_unwrap.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_unwrap.js new file mode 100644 index 0000000..54f46e9 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/advanced_unwrap.js @@ -0,0 +1,666 @@ +/** + * Full HTML5 compatibility rule set + * These rules define which tags and CSS classes are supported and which tags should be specially treated. + * + * Examples based on this rule set: + * + * foo + * ... becomes ... + * foo + * + * + * ... becomes ... + * + * + *
foo
+ * ... becomes ... + *
foo
+ * + * foo + * ... becomes ... + * foo + * + * foo
bar + * ... becomes ... + * foo
bar + * + *
hello
+ * ... becomes ... + *
hello
+ * + *
hello
+ * ... becomes ... + *
hello
+ */ +var wysihtml5ParserRules = { + /** + * CSS Class white-list + * Following CSS classes won't be removed when parsed by the wysihtml5 HTML parser + * If all classes should pass "any" as classes value. Ex: "classes": "any" + */ + "classes": { + "wysiwyg-clear-both": 1, + "wysiwyg-clear-left": 1, + "wysiwyg-clear-right": 1, + "wysiwyg-color-aqua": 1, + "wysiwyg-color-black": 1, + "wysiwyg-color-blue": 1, + "wysiwyg-color-fuchsia": 1, + "wysiwyg-color-gray": 1, + "wysiwyg-color-green": 1, + "wysiwyg-color-lime": 1, + "wysiwyg-color-maroon": 1, + "wysiwyg-color-navy": 1, + "wysiwyg-color-olive": 1, + "wysiwyg-color-purple": 1, + "wysiwyg-color-red": 1, + "wysiwyg-color-silver": 1, + "wysiwyg-color-teal": 1, + "wysiwyg-color-white": 1, + "wysiwyg-color-yellow": 1, + "wysiwyg-float-left": 1, + "wysiwyg-float-right": 1, + "wysiwyg-font-size-large": 1, + "wysiwyg-font-size-larger": 1, + "wysiwyg-font-size-medium": 1, + "wysiwyg-font-size-small": 1, + "wysiwyg-font-size-smaller": 1, + "wysiwyg-font-size-x-large": 1, + "wysiwyg-font-size-x-small": 1, + "wysiwyg-font-size-xx-large": 1, + "wysiwyg-font-size-xx-small": 1, + "wysiwyg-text-align-center": 1, + "wysiwyg-text-align-justify": 1, + "wysiwyg-text-align-left": 1, + "wysiwyg-text-align-right": 1 + }, + + + "type_definitions": { + + "visible_content_object": { + "methods": { + "has_visible_contet": 1 + } + }, + + "alignment_object": { + "classes": { + "wysiwyg-text-align-center": 1, + "wysiwyg-text-align-justify": 1, + "wysiwyg-text-align-left": 1, + "wysiwyg-text-align-right": 1, + "wysiwyg-float-left": 1, + "wysiwyg-float-right": 1 + }, + "styles": { + "float": ["left", "right"], + "text-align": ["left", "right", "center"] + } + }, + + "valid_image_src": { + "attrs": { + "src": /^[^data\:]/i + } + }, + + "text_color_object": { + "styles": { + "color": true, + "background-color": true + } + }, + + "text_fontsize_object": { + "styles": { + "font-size": true + } + }, + + "text_formatting_object": { + "classes": { + "wysiwyg-color-aqua": 1, + "wysiwyg-color-black": 1, + "wysiwyg-color-blue": 1, + "wysiwyg-color-fuchsia": 1, + "wysiwyg-color-gray": 1, + "wysiwyg-color-green": 1, + "wysiwyg-color-lime": 1, + "wysiwyg-color-maroon": 1, + "wysiwyg-color-navy": 1, + "wysiwyg-color-olive": 1, + "wysiwyg-color-purple": 1, + "wysiwyg-color-red": 1, + "wysiwyg-color-silver": 1, + "wysiwyg-color-teal": 1, + "wysiwyg-color-white": 1, + "wysiwyg-color-yellow": 1, + "wysiwyg-font-size-large": 1, + "wysiwyg-font-size-larger": 1, + "wysiwyg-font-size-medium": 1, + "wysiwyg-font-size-small": 1, + "wysiwyg-font-size-smaller": 1, + "wysiwyg-font-size-x-large": 1, + "wysiwyg-font-size-x-small": 1, + "wysiwyg-font-size-xx-large": 1, + "wysiwyg-font-size-xx-small": 1 + } + } + }, + + "comments": 1, // if set allows comments to pass + + /** + * Tag list + * + * The following options are available: + * + * - add_class: converts and deletes the given HTML4 attribute (align, clear, ...) via the given method to a css class + * The following methods are implemented in wysihtml5.dom.parse: + * - align_text: converts align attribute values (right/left/center/justify) to their corresponding css class "wysiwyg-text-align-*") + *

foo

... becomes ...

foo

+ * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*" + *
... becomes ...
+ * - align_img: converts align attribute values (right/left) on to their corresponding css class "wysiwyg-float-*" + * + * - add_style: converts and deletes the given HTML4 attribute (align) via the given method to a css style + * The following methods are implemented in wysihtml5.dom.parse: + * - align_text: converts align attribute values (right/left/center) to their corresponding css style) + *

foo

... becomes ...

foo

+ * + * - remove: removes the element and its content + * + * - unwrap removes element but leaves content + * + * - rename_tag: renames the element to the given tag + * + * - set_class: adds the given class to the element (note: make sure that the class is in the "classes" white list above) + * + * - set_attributes: sets/overrides the given attributes + * + * - check_attributes: checks the given HTML attribute via the given method + * - url: allows only valid urls (starting with http:// or https://) + * - src: allows something like "/foobar.jpg", "http://google.com", ... + * - href: allows something like "mailto:bert@foo.com", "http://google.com", "/foobar.jpg" + * - alt: strips unwanted characters. if the attribute is not set, then it gets set (to ensure valid and compatible HTML) + * - numbers: ensures that the attribute only contains numeric characters + * - any: allows anything to pass + */ + "tags": { + "tr": { + "add_class": { + "align": "align_text" + } + }, + "strike": { + "unwrap": 1 + }, + "form": { + "unwrap": 1 + }, + "rt": { + "rename_tag": "span" + }, + "code": {}, + "acronym": { + "rename_tag": "span" + }, + "br": { + "add_class": { + "clear": "clear_br" + } + }, + "details": { + "unwrap": 1 + }, + "h4": { + "add_class": { + "align": "align_text" + } + }, + "em": {}, + "title": { + "remove": 1 + }, + "multicol": { + "unwrap": 1 + }, + "figure": { + "unwrap": 1 + }, + "xmp": { + "unwrap": 1 + }, + "small": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-smaller" + }, + "area": { + "remove": 1 + }, + "time": { + "unwrap": 1 + }, + "dir": { + "rename_tag": "ul" + }, + "bdi": { + "unwrap": 1 + }, + "command": { + "unwrap": 1 + }, + "ul": {}, + "progress": { + "rename_tag": "span" + }, + "dfn": { + "unwrap": 1 + }, + "iframe": { + "remove": 1 + }, + "figcaption": { + "unwrap": 1 + }, + "a": { + "check_attributes": { + "href": "href", // if you compiled master manually then change this from 'url' to 'href' + "target": "any" + }, + "set_attributes": { + "rel": "nofollow" + } + }, + "img": { + "one_of_type": { + "valid_image_src": 1 + }, + "check_attributes": { + "width": "numbers", + "alt": "alt", + "src": "src", // if you compiled master manually then change this from 'url' to 'src' + "height": "numbers" + }, + "add_class": { + "align": "align_img" + } + }, + "rb": { + "unwrap": 1 + }, + "footer": { + "rename_tag": "div" + }, + "noframes": { + "remove": 1 + }, + "abbr": { + "unwrap": 1 + }, + "u": {}, + "bgsound": { + "remove": 1 + }, + "sup": { + "unwrap": 1 + }, + "address": { + "unwrap": 1 + }, + "basefont": { + "remove": 1 + }, + "nav": { + "unwrap": 1 + }, + "h1": { + "add_class": { + "align": "align_text" + } + }, + "head": { + "unwrap": 1 + }, + "tbody": { + "add_class": { + "align": "align_text" + } + }, + "dd": { + "unwrap": 1 + }, + "s": { + "unwrap": 1 + }, + "li": {}, + "td": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers", + "valign": "any", + "align": "any" + }, + "add_class": { + "align": "align_text" + } + }, + "object": { + "remove": 1 + }, + + "div": { + "one_of_type": { + "visible_content_object": 1 + }, + "remove_action": "unwrap", + "keep_styles": { + "textAlign": 1, + "float": 1 + }, + "add_class": { + "align": "align_text" + } + }, + + "option": { + "remove":1 + }, + "select": { + "remove":1 + }, + "i": {}, + "track": { + "remove": 1 + }, + "wbr": { + "remove": 1 + }, + "fieldset": { + "unwrap": 1 + }, + "big": { + "rename_tag": "span", + "set_class": "wysiwyg-font-size-larger" + }, + "button": { + "unwrap": 1 + }, + "noscript": { + "remove": 1 + }, + "svg": { + "remove": 1 + }, + "input": { + "remove": 1 + }, + "table": {}, + "keygen": { + "remove": 1 + }, + "h5": { + "add_class": { + "align": "align_text" + } + }, + "meta": { + "remove": 1 + }, + "map": { + "remove": 1 + }, + "isindex": { + "remove": 1 + }, + "mark": { + "unwrap": 1 + }, + "caption": { + "add_class": { + "align": "align_text" + } + }, + "tfoot": { + "add_class": { + "align": "align_text" + } + }, + "base": { + "remove": 1 + }, + "video": { + "remove": 1 + }, + "strong": {}, + "canvas": { + "remove": 1 + }, + "output": { + "unwrap": 1 + }, + "marquee": { + "unwrap": 1 + }, + "b": {}, + "q": { + "check_attributes": { + "cite": "url" + } + }, + "applet": { + "remove": 1 + }, + "span": { + "one_of_type": { + "text_formatting_object": 1, + "text_color_object": 1, + "text_fontsize_object": 1 + }, + "keep_styles": { + "color": 1, + "backgroundColor": 1, + "fontSize": 1 + }, + "remove_action": "unwrap" + }, + "rp": { + "unwrap": 1 + }, + "spacer": { + "remove": 1 + }, + "source": { + "remove": 1 + }, + "aside": { + "rename_tag": "div" + }, + "frame": { + "remove": 1 + }, + "section": { + "rename_tag": "div" + }, + "body": { + "unwrap": 1 + }, + "ol": {}, + "nobr": { + "unwrap": 1 + }, + "html": { + "unwrap": 1 + }, + "summary": { + "unwrap": 1 + }, + "var": { + "unwrap": 1 + }, + "del": { + "unwrap": 1 + }, + "blockquote": { + "check_attributes": { + "cite": "url" + } + }, + "style": { + "remove": 1 + }, + "device": { + "remove": 1 + }, + "meter": { + "unwrap": 1 + }, + "h3": { + "add_class": { + "align": "align_text" + } + }, + "textarea": { + "unwrap": 1 + }, + "embed": { + "remove": 1 + }, + "hgroup": { + "unwrap": 1 + }, + "font": { + "rename_tag": "span", + "add_class": { + "size": "size_font" + } + }, + "tt": { + "unwrap": 1 + }, + "noembed": { + "remove": 1 + }, + "thead": { + "add_class": { + "align": "align_text" + } + }, + "blink": { + "unwrap": 1 + }, + "plaintext": { + "unwrap": 1 + }, + "xml": { + "remove": 1 + }, + "h6": { + "add_class": { + "align": "align_text" + } + }, + "param": { + "remove": 1 + }, + "th": { + "check_attributes": { + "rowspan": "numbers", + "colspan": "numbers" + }, + "add_class": { + "align": "align_text" + } + }, + "legend": { + "unwrap": 1 + }, + "hr": {}, + "label": { + "unwrap": 1 + }, + "dl": { + "unwrap": 1 + }, + "kbd": { + "unwrap": 1 + }, + "listing": { + "unwrap": 1 + }, + "dt": { + "unwrap": 1 + }, + "nextid": { + "remove": 1 + }, + "pre": {}, + "center": { + "rename_tag": "div", + "set_class": "wysiwyg-text-align-center" + }, + "audio": { + "remove": 1 + }, + "datalist": { + "unwrap": 1 + }, + "samp": { + "unwrap": 1 + }, + "col": { + "remove": 1 + }, + "article": { + "rename_tag": "div" + }, + "cite": {}, + "link": { + "remove": 1 + }, + "script": { + "remove": 1 + }, + "bdo": { + "unwrap": 1 + }, + "menu": { + "rename_tag": "ul" + }, + "colgroup": { + "remove": 1 + }, + "ruby": { + "unwrap": 1 + }, + "h2": { + "add_class": { + "align": "align_text" + } + }, + "ins": { + "unwrap": 1 + }, + "p": { + "add_class": { + "align": "align_text" + } + }, + "sub": { + "unwrap": 1 + }, + "comment": { + "remove": 1 + }, + "frameset": { + "remove": 1 + }, + "optgroup": { + "unwrap": 1 + }, + "header": { + "rename_tag": "div" + } + } +}; \ No newline at end of file diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/simple.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/simple.js new file mode 100644 index 0000000..63a2e8b --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/parser_rules/simple.js @@ -0,0 +1,32 @@ +/** + * Very simple basic rule set + * + * Allows + * , , , ,

,

, ,
, ,
    ,
      ,
    • + * + * For a proper documentation of the format check advanced.js + */ +var wysihtml5ParserRules = { + tags: { + strong: {}, + b: {}, + i: {}, + em: {}, + br: {}, + p: {}, + div: {}, + span: {}, + ul: {}, + ol: {}, + li: {}, + a: { + set_attributes: { + target: "_blank", + rel: "nofollow" + }, + check_attributes: { + href: "url" // important to avoid XSS + } + } + } +}; \ No newline at end of file diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/assert/html_equal.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/assert/html_equal.js new file mode 100644 index 0000000..8402367 --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/assert/html_equal.js @@ -0,0 +1,103 @@ +var wysihtml5 = wysihtml5 || {}; +wysihtml5.assert = wysihtml5.assert || {}; + +/** + * Compare html strings without stumbling upon browser misbehaviors + * Uses and takes the same parameters as QUnit's equal method + * + * @example + * wysihtml5.assert.htmlEqual( + * removeAttributes('

      foo

      '), + * '

      foo

      ', + * 'Removed align attribute on

      ' + * ); + */ +wysihtml5.assert.htmlEqual = (function() { + var htmlHost = document.createElement("div"); + + /** + * IE uppercases tags and attribute names + * and also removes quotes around attribute values whenever possible + */ + var NEEDS_TO_BE_PREPARSED = (function() { + var html = 'foo'; + htmlHost.innerHTML = html; + return htmlHost.innerHTML != html; + })(); + + var DOESNT_PRESERVE_WHITE_SPACE = (function() { + var div = document.createElement("div"), + a = document.createElement("a"), + p = document.createElement("p"); + a.appendChild(p); + div.appendChild(a); + return div.innerHTML.toLowerCase() != "

      "; + })(); + + /** + * Browsers don't preserve original attribute order + * In order to be able to compare html we simply split both, the expected and actual html at spaces and element-ends, + * sort them alphabetically and put them back together + * TODO: This solution is a bit crappy. Maybe there's a smarter way. However it works for now. + */ + var tokenizeHTML = (function() { + var REG_EXP = /\s+|\>|]*>)([\S\s]*?)(<\/pre>)/mgi, + WHITE_SPACE_REG_EXP = /\s+/gm, + PLACEHOLDER = "___PRE_CONTENT___", + PLACEHOLDER_REG_EXP = new RegExp(PLACEHOLDER, "g"); + return function(html) { + var preContents = []; + // Extract content of elements that preserve white space first + html = html.replace(PRE_REG_EXP, function(match, $1, $2, $3) { + preContents.push($2); + return $1 + PLACEHOLDER + $3; + }); + + // Normalize space + html = html.replace(WHITE_SPACE_REG_EXP, " "); + + // Reinsert original pre content + html = html.replace(PLACEHOLDER_REG_EXP, function() { + return preContents.shift(); + }); + + return html; + }; + })(); + + var removeWhiteSpace = (function() { + var REG_EXP = /(>)(\s*?)(<)/gm; + return function(html) { + return wysihtml5.lang.string(html.replace(REG_EXP, "$1$3")).trim(); + }; + })(); + + return function(actual, expected, message, config) { + config = config || {}; + if (NEEDS_TO_BE_PREPARSED) { + actual = wysihtml5.dom.getAsDom(actual).innerHTML; + expected = wysihtml5.dom.getAsDom(expected).innerHTML; + } + + if (config.normalizeWhiteSpace || DOESNT_PRESERVE_WHITE_SPACE) { + actual = normalizeWhiteSpace(actual); + expected = normalizeWhiteSpace(expected); + } + + if (config.removeWhiteSpace) { + actual = removeWhiteSpace(actual); + expected = removeWhiteSpace(expected); + } + + actual = tokenizeHTML(actual); + expected = tokenizeHTML(expected); + ok(actual == expected, message); + }; +})(); diff --git a/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/browser.js b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/browser.js new file mode 100644 index 0000000..60771ef --- /dev/null +++ b/karmaworld/apps/wysihtml5/static/wysihtml5/wysihtml-0.4.17/src/browser.js @@ -0,0 +1,387 @@ +/** + * Detect browser support for specific features + */ +wysihtml5.browser = (function() { + var userAgent = navigator.userAgent, + testElement = document.createElement("div"), + // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect + isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, + isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, + isChrome = userAgent.indexOf("Chrome/") !== -1, + isOpera = userAgent.indexOf("Opera/") !== -1; + + function iosVersion(userAgent) { + return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1]; + } + + function androidVersion(userAgent) { + return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1]; + } + + function isIE(version, equation) { + var rv = -1, + re; + + if (navigator.appName == 'Microsoft Internet Explorer') { + re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + } else if (navigator.appName == 'Netscape') { + re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); + } + + if (re && re.exec(navigator.userAgent) != null) { + rv = parseFloat(RegExp.$1); + } + + if (rv === -1) { return false; } + if (!version) { return true; } + if (!equation) { return version === rv; } + if (equation === "<") { return version < rv; } + if (equation === ">") { return version > rv; } + if (equation === "<=") { return version <= rv; } + if (equation === ">=") { return version >= rv; } + } + + return { + // Static variable needed, publicly accessible, to be able override it in unit tests + USER_AGENT: userAgent, + + /** + * Exclude browsers that are not capable of displaying and handling + * contentEditable as desired: + * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable + * - IE < 8 create invalid markup and crash randomly from time to time + * + * @return {Boolean} + */ + supported: function() { + var userAgent = this.USER_AGENT.toLowerCase(), + // Essential for making html elements editable + hasContentEditableSupport = "contentEditable" in testElement, + // Following methods are needed in order to interact with the contentEditable area + hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, + // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ + hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, + // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) + isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; + return hasContentEditableSupport + && hasEditingApiSupport + && hasQuerySelectorSupport + && !isIncompatibleMobileBrowser; + }, + + isTouchDevice: function() { + return this.supportsEvent("touchmove"); + }, + + isIos: function() { + return (/ipad|iphone|ipod/i).test(this.USER_AGENT); + }, + + isAndroid: function() { + return this.USER_AGENT.indexOf("Android") !== -1; + }, + + /** + * Whether the browser supports sandboxed iframes + * Currently only IE 6+ offers such feature @@ -30,4 +32,4 @@
- \ No newline at end of file + diff --git a/requirements.txt b/requirements.txt index e28ba4c..f758b14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,6 @@ cssmin==0.1.4 amqplib==1.0.2 django-sslify>=0.2 newrelic==2.40.0.34 +bleach==1.4 +bleach-whitelist==0.0.4 +Markdown==2.5.2 -- 2.25.1