Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

wysihtml5 v0.2.0 - now library agnostic

  • Loading branch information...
commit 623afa8b624da0dcde8c88471e9b83126c938dbb 1 parent 64b67e3
Christopher Blum authored
Showing with 10,554 additions and 8,329 deletions.
  1. +1 −1  LICENSE
  2. +53 −29 Makefile
  3. +20 −4 README.textile
  4. +82 −0 build/minify.js
  5. +4,224 −3,741 dist/{wysihtml5-0.1.0.js → wysihtml5-0.2.0.js}
  6. +242 −0 dist/wysihtml5-0.2.0.min.js
  7. +15 −12 examples/advanced.html
  8. +128 −2 examples/css/stylesheet.css
  9. +0 −1  examples/rules/spec.json
  10. +11 −19 examples/simple.html
  11. +139 −0 lib/base/base.js
  12. +0 −461 lib/rangy/rangy-cssclassapplier-wysihtml5.js
  13. +563 −0 parser_rules/advanced.js
  14. +32 −0 parser_rules/simple.js
  15. +7 −11 src/assert/html_equals.js
  16. +318 −0 src/browser.js
  17. +0 −267 src/browser_support.js
  18. +3 −4 src/commands.js
  19. +20 −23 src/commands/bold.js
  20. +54 −57 src/commands/createLink.js
  21. +15 −19 src/commands/fontSize.js
  22. +15 −20 src/commands/foreColor.js
  23. +95 −99 src/commands/formatBlock.js
  24. +42 −47 src/commands/formatInline.js
  25. +17 −21 src/commands/insertHTML.js
  26. +93 −100 src/commands/insertImage.js
  27. +20 −24 src/commands/insertLineBreak.js
  28. +55 −59 src/commands/insertOrderedList.js
  29. +56 −60 src/commands/insertUnorderedList.js
  30. +19 −23 src/commands/italic.js
  31. +14 −18 src/commands/justifyCenter.js
  32. +14 −18 src/commands/justifyLeft.js
  33. +14 −18 src/commands/justifyRight.js
  34. +14 −18 src/commands/underline.js
  35. +24 −23 src/{utils → dom}/auto_link.js
  36. +31 −0 src/dom/class.js
  37. +2 −2 src/{utils → dom}/contains.js
  38. +6 −6 src/{utils/convert_into_list.js → dom/convert_to_list.js}
  39. +11 −11 src/{utils → dom}/copy_attributes.js
  40. +73 −0 src/dom/copy_styles.js
  41. +26 −0 src/dom/delegate.js
  42. +5 −2 src/{utils/get_in_dom_element.js → dom/get_as_dom.js}
  43. +6 −6 src/{utils → dom}/get_parent_element.js
  44. +69 −0 src/dom/get_style.js
  45. +9 −9 src/{utils → dom}/has_element_with_class_name.js
  46. +3 −3 src/{utils → dom}/has_element_with_tag_name.js
  47. +15 −0 src/dom/insert.js
  48. +4 −16 src/{utils/insert_rules.js → dom/insert_css.js}
  49. +53 −0 src/dom/observe.js
  50. +28 −51 src/{utils/sanitize_html.js → dom/parse.js}
  51. +4 −4 src/{utils → dom}/remove_empty_text_nodes.js
  52. +3 −3 src/{utils → dom}/rename_element.js
  53. +2 −2 src/{utils/unwrap.js → dom/replace_with_child_nodes.js}
  54. +6 −6 src/{utils → dom}/resolve_list.js
  55. +258 −0 src/dom/sandbox.js
  56. +14 −0 src/dom/set_attributes.js
  57. +19 −0 src/dom/set_styles.js
  58. +42 −0 src/dom/simulate_placeholder.js
  59. +29 −0 src/dom/text_content.js
  60. +29 −68 src/editor.js
  61. +58 −0 src/lang/array.js
  62. +40 −0 src/lang/dispatcher.js
  63. +42 −0 src/lang/object.js
  64. +42 −0 src/lang/string.js
  65. +2 −2 src/quirks/clean_pasted_html.js
  66. +68 −63 src/quirks/ensure_proper_clearing.js
  67. +30 −0 src/quirks/get_correct_inner_html.js
  68. +20 −19 src/quirks/insert_line_break_on_return.js
  69. +6 −5 src/quirks/redraw.js
  70. +434 −0 src/selection/html_applier.js
  71. +450 −0 src/selection/selection.js
  72. +151 −130 src/toolbar/dialog.js
  73. +39 −32 src/toolbar/speech.js
  74. +229 −204 src/toolbar/toolbar.js
  75. +0 −16 src/utils/auto_focus.js
  76. +0 −451 src/utils/caret.js
  77. +0 −76 src/utils/copy_styles.js
  78. +0 −50 src/utils/get_style.js
  79. +0 −46 src/utils/observe.js
  80. +0 −240 src/utils/sandbox.js
  81. +0 −39 src/utils/simulate_placeholder.js
  82. +0 −102 src/utils/synchronizer.js
  83. +0 −25 src/utils/text_content.js
  84. +278 −256 src/views/composer.js
  85. +143 −142 src/views/composer.observe.js
  86. +170 −159 src/views/composer.style.js
  87. +97 −0 src/views/synchronizer.js
  88. +8 −11 src/views/textarea.js
  89. +14 −14 src/views/view.js
  90. +32 −6 src/wysihtml5.js
  91. +6 −6 test/assert/html_equals_test.js
  92. +25 −25 test/{browser_support_test.js → browser_test.js}
  93. +4 −4 test/{utils → dom}/auto_link_test.js
  94. +18 −0 test/dom/contains_test.js
  95. +86 −0 test/dom/convert_to_list_test.js
  96. +19 −17 test/{utils → dom}/copy_attributes_test.js
  97. +111 −0 test/dom/copy_styles_test.js
  98. +62 −0 test/dom/delegate_test.js
  99. +14 −11 test/{utils/get_in_dom_element_test.js → dom/get_as_dom_test.js}
  100. +37 −36 test/{utils → dom}/get_parent_element_test.js
  101. +8 −8 test/{utils → dom}/get_style_test.js
  102. +30 −0 test/dom/has_element_with_class_name_test.js
  103. +26 −0 test/dom/has_element_with_tag_name_test.js
  104. +56 −0 test/dom/insert_css_test.js
  105. +83 −0 test/dom/observe_test.js
  106. +35 −6 test/{utils/sanitize_html_test.js → dom/parse_test.js}
  107. +3 −3 test/{utils → dom}/rename_element_test.js
  108. +6 −15 test/{utils → dom}/resolve_list_test.js
  109. +65 −59 test/{utils → dom}/sandbox_test.js
  110. +15 −0 test/dom/set_attributes_test.js
  111. +19 −0 test/dom/set_styles_test.js
  112. +146 −257 test/editor_test.js
  113. +61 −0 test/incompatible_test.js
  114. +35 −21 test/index.html
  115. +22 −0 test/lang/array_test.js
  116. +22 −0 test/lang/object_test.js
  117. +19 −0 test/lang/string_test.js
  118. +1 −1  test/quirks/clean_pasted_html_test.js
  119. +0 −95 test/utils/convert_into_list_test.js
  120. +0 −110 test/utils/copy_styles_test.js
  121. +0 −27 test/utils/has_element_with_class_name_test.js
  122. +0 −23 test/utils/has_element_with_tag_name_test.js
  123. +0 −53 test/utils/insert_rules_test.js
  124. +0 −75 test/utils/observe_test.js
  125. +1 −1  version.txt
View
2  LICENSE
@@ -1,4 +1,4 @@
-Copyright (C) 2009 XING AG
+Copyright (C) 2011 XING AG
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or any later version.
View
82 Makefile
@@ -2,41 +2,51 @@ VERSION = $(shell cat version.txt)
JS_OUTPUT = "dist/wysihtml5-${VERSION}.js"
-JS_FILES = lib/rangy/rangy-core.js \
- lib/rangy/rangy-cssclassapplier-wysihtml5.js \
- src/wysihtml5.js \
- src/browser_support.js \
- src/utils/auto_focus.js \
- src/utils/auto_link.js \
- src/utils/caret.js \
- src/utils/contains.js \
- src/utils/convert_into_list.js \
- src/utils/copy_styles.js \
- src/utils/copy_attributes.js \
- src/utils/get_in_dom_element.js \
- src/utils/get_parent_element.js \
- src/utils/get_style.js \
- src/utils/has_element_with_tag_name.js \
- src/utils/has_element_with_class_name.js \
- src/utils/insert_rules.js \
- src/utils/observe.js \
- src/utils/resolve_list.js \
- src/utils/rename_element.js \
- src/utils/remove_empty_text_nodes.js \
- src/utils/sandbox.js \
- src/utils/sanitize_html.js \
- src/utils/simulate_placeholder.js \
- src/utils/synchronizer.js \
- src/utils/text_content.js \
- src/utils/unwrap.js \
+JS_FILES = src/wysihtml5.js \
+ lib/rangy/rangy-core.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/get_as_dom.js \
+ src/dom/get_parent_element.js \
+ src/dom/get_style.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/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/set_attributes.js \
+ src/dom/set_styles.js \
+ src/dom/simulate_placeholder.js \
+ src/dom/text_content.js \
src/quirks/clean_pasted_html.js \
src/quirks/ensure_proper_clearing.js \
+ src/quirks/get_correct_inner_html.js \
src/quirks/insert_line_break_on_return.js \
src/quirks/redraw.js \
+ src/selection/selection.js \
+ src/selection/html_applier.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/toolbar/dialog.js \
src/toolbar/speech.js \
@@ -60,6 +70,20 @@ JS_FILES = lib/rangy/rangy-core.js \
src/commands/underline.js \
src/editor.js
-all:
+all: bundle minify
+
+bundle:
+ @@echo "Bundling..."
+ @@touch ${JS_OUTPUT}
@@rm ${JS_OUTPUT}
- @@cat ${JS_FILES} >> ${JS_OUTPUT}
+ @@cat ${JS_FILES} >> ${JS_OUTPUT}
+ @@cat ${JS_OUTPUT} | sed "s/@VERSION/${VERSION}/" > "${JS_OUTPUT}.tmp"
+ @@mv "${JS_OUTPUT}.tmp" ${JS_OUTPUT}
+
+minify:
+ @@echo "Minifying... (this requires node.js)"
+ @@node build/minify.js ${JS_OUTPUT}
+ @@echo "Done."
+
+unittest:
+ @@open test/index.html
View
24 README.textile
@@ -1,8 +1,9 @@
-h1. wysihtml5 0.1.0
+h1. wysihtml5 0.2.0
wysihtml5 is an open source rich text editor based on HTML5 technology and the progressive-enhancement approach.
-It uses a sophisticated security concept and aims to generate fully valid HTML5 markup by preventing unmaintainable tag-soups and inline-styles.
-The editor is already in production use on "XING.com":https://www.xing.com - a social network with more than 10 million members.
+It uses a sophisticated security concept and aims to generate fully valid HTML5 markup by preventing unmaintainable tag soups and inline styles.
+The code is completely library agnostic: No jQuery, Prototype or similar is required.
+The editor is already in use on "XING.com":https://www.xing.com - a social network with more than 10 million members.
h2. Features:
@@ -15,6 +16,7 @@ h2. Features:
* 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 11+
h2. Browser Support
@@ -44,7 +46,7 @@ Following is a list of all configuration options with their corresponding defaul
// Object which includes parser rules (set this to examples/rules/spec.json or your own spec, otherwise only span tags are allowed!)
parserRules: null,
// Parser method to use when the user inserts content via copy & paste
- parser: wysihtml5.utils.sanitizeHTML || Prototype.K,
+ parser: wysihtml5.dom.parse || Prototype.K,
// Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
composerClassName: "wysihtml5-editor",
// Class name to add to the body when the wysihtml5 editor is supported
@@ -113,6 +115,20 @@ copied to the hidden <code><textarea></code>)
behavior on the iframe's <code><body></code>
# Checks whether a toolbar is given and sets event listeners on it's link
+h2. How to build your own wysihtml5 files
+
+Clone and build the js file:
+<code>
+ git clone git://github.com/xing/wysihtml5.git
+ cd wysihtml5
+ make
+</code>
+
+Run the unit tests:
+<code>
+ make unittest
+</code>
+
h2. Research Material
Before starting this library we spent a lot of time investigating the different browsers and their behaviors.
View
82 build/minify.js
@@ -0,0 +1,82 @@
+var script = process.argv[2],
+ http = require("http"),
+ queryString = require("querystring"),
+ fs = require("fs");
+
+if (!script) {
+ throw "No script url given";
+}
+
+function post(code, callback) {
+ // Build the post string from an object
+ var postData = queryString.stringify({
+ compilation_level: "SIMPLE_OPTIMIZATIONS",
+ output_format: "text",
+ output_info: "compiled_code",
+ warning_level: "QUIET",
+ js_code: code
+ });
+
+ // An object of options to indicate where to post to
+ var postOptions = {
+ host: "closure-compiler.appspot.com",
+ port: "80",
+ path: "/compile",
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Content-Length": postData.length
+ }
+ };
+
+ // Set up the request
+ var request = http.request(postOptions, function(response) {
+ var responseText = [];
+ response.setEncoding("utf8");
+ response.on("data", function(data) {
+ responseText.push(data);
+ });
+ response.on("end", function() {
+ callback(responseText.join(""));
+ });
+ });
+
+ // Post the data
+ request.write(postData);
+ request.end();
+}
+
+function readFile(filePath, callback) {
+ // This is an async file read
+ fs.readFile(filePath, "utf-8", function (err, data) {
+ if (err) {
+ // If this were just a small part of the application, you would
+ // want to handle this differently, maybe throwing an exception
+ // for the caller to handle. Since the file is absolutely essential
+ // to the program's functionality, we're going to exit with a fatal
+ // error instead.
+ console.log("FATAL An error occurred trying to read in the file: " + err);
+ process.exit(-2);
+ }
+ // Make sure there's data before we post it
+ if (data) {
+ callback(data);
+ } else {
+ console.log("No data to post");
+ process.exit(-1);
+ }
+ });
+}
+
+function writeFile(filePath, data, callback) {
+ fs.writeFile(filePath, data, "utf-8", callback);
+}
+
+
+// Ok GO!
+readFile(script, function(code) {
+ post(code, function(code) {
+ var output = script.replace(/\.js/, ".min.js");
+ writeFile(output, code);
+ });
+});
View
7,965 dist/wysihtml5-0.1.0.js → dist/wysihtml5-0.2.0.js
4,224 additions, 3,741 deletions not shown
View
242 dist/wysihtml5-0.2.0.min.js
242 additions, 0 deletions not shown
View
27 examples/advanced.html
@@ -38,7 +38,7 @@
#toolbar,
textarea {
- width: 600px;
+ width: 800px;
padding: 5px;
box-sizing: boder-box;
-webkit-box-sizing: border-box;
@@ -47,7 +47,6 @@
}
textarea {
- width: 600px;
height: 280px;
border: 2px solid green;
font-family: Verdana;
@@ -92,6 +91,8 @@
<a data-wysihtml5-command="insertImage">insert image</a> |
<a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">h1</a> |
<a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h2">h2</a> |
+ <a data-wysihtml5-command="insertUnorderedList">insertUnorderedList</a> |
+ <a data-wysihtml5-command="insertOrderedList">insertUnorderedList</a> |
<a data-wysihtml5-command="foreColor" data-wysihtml5-command-value="red">red</a> |
<a data-wysihtml5-command="foreColor" data-wysihtml5-command-value="green">green</a> |
<a data-wysihtml5-command="foreColor" data-wysihtml5-command-value="blue">blue</a>
@@ -131,32 +132,34 @@
<small>powered by <a href="https://github.com/xing/wysihtml5" target="_blank">wysihtml5</a>.</small>
-<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"></script>
-<script type="text/javascript" src="../dist/wysihtml5-0.1.0.js"></script>
+<script src="../parser_rules/advanced.js"></script>
+<script src="../dist/wysihtml5-0.2.0.js"></script>
<script>
var editor = new wysihtml5.Editor("textarea", {
toolbar: "toolbar",
- parserRules: "rules/spec.json",
- stylesheets: "css/stylesheet.css"
+ stylesheets: "css/stylesheet.css",
+ parserRules: wysihtml5ParserRules
});
+ var log = document.getElementById("log");
+
editor
.observe("load", function() {
- $("log").insert("<div>load</div>");
+ log.innerHTML += "<div>load</div>";
})
.observe("focus", function() {
- $("log").insert("<div>focus</div>");
+ log.innerHTML += "<div>focus</div>";
})
.observe("blur", function() {
- $("log").insert("<div>blur</div>");
+ log.innerHTML += "<div>blur</div>";
})
.observe("change", function() {
- $("log").insert("<div>change</div>");
+ log.innerHTML += "<div>change</div>";
})
.observe("paste", function() {
- $("log").insert("<div>paste</div>");
+ log.innerHTML += "<div>paste</div>";
})
.observe("newword:composer", function() {
- $("log").insert("<div>newword:composer</div>");
+ log.innerHTML += "<div>newword:composer</div>";
});
</script>
View
130 examples/css/stylesheet.css
@@ -1,11 +1,137 @@
+.wysiwyg-font-size-smaller {
+ font-size: smaller;
+}
+
+.wysiwyg-font-size-larger {
+ font-size: larger;
+}
+
+.wysiwyg-font-size-xx-large {
+ font-size: xx-large;
+}
+
+.wysiwyg-font-size-x-large {
+ font-size: x-large;
+}
+
+.wysiwyg-font-size-large {
+ font-size: large;
+}
+
+.wysiwyg-font-size-medium {
+ font-size: medium;
+}
+
+.wysiwyg-font-size-small {
+ font-size: small;
+}
+
+.wysiwyg-font-size-x-small {
+ font-size: x-small;
+}
+
+.wysiwyg-font-size-xx-small {
+ font-size: xx-small;
+}
+
+.wysiwyg-color-black {
+ color: black;
+}
+
+.wysiwyg-color-silver {
+ color: silver;
+}
+
+.wysiwyg-color-gray {
+ color: gray;
+}
+
+.wysiwyg-color-white {
+ color: white;
+}
+
+.wysiwyg-color-maroon {
+ color: maroon;
+}
+
.wysiwyg-color-red {
color: red;
}
-.wysiwyg-color-blue {
- color: blue;
+.wysiwyg-color-purple {
+ color: purple;
+}
+
+.wysiwyg-color-fuchsia {
+ color: fuchsia;
}
.wysiwyg-color-green {
color: green;
+}
+
+.wysiwyg-color-lime {
+ color: lime;
+}
+
+.wysiwyg-color-olive {
+ color: olive;
+}
+
+.wysiwyg-color-yellow {
+ color: yellow;
+}
+
+.wysiwyg-color-navy {
+ color: navy;
+}
+
+.wysiwyg-color-blue {
+ color: blue;
+}
+
+.wysiwyg-color-teal {
+ color: teal;
+}
+
+.wysiwyg-color-aqua {
+ color: aqua;
+}
+
+.wysiwyg-color-xing {
+ color: #006567;
+}
+
+.wysiwyg-text-align-right {
+ text-align: right;
+}
+
+.wysiwyg-text-align-center {
+ text-align: center;
+}
+
+.wysiwyg-text-align-left {
+ text-align: left;
+}
+
+.wysiwyg-text-decoration-underline {
+ text-decoration: underline;
+}
+
+.wysiwyg-float-left {
+ float: left;
+ margin: 0 8px 8px 0;
+}
+
+.wysiwyg-float-right {
+ float: right;
+ margin: 0 0 8px 8px;
+}
+
+.wysiwyg-clear-right {
+ clear: right;
+}
+
+.wysiwyg-clear-left {
+ clear: left;
}
View
1  examples/rules/spec.json
@@ -1 +0,0 @@
-{"classes":{"wysiwyg-font-size-x-large":1,"wysiwyg-font-size-small":1,"wysiwyg-color-yellow":1,"wysiwyg-color-purple":1,"wysiwyg-font-size-x-small":1,"wysiwyg-color-olive":1,"wysiwyg-clear-right":1,"wysiwyg-text-decoration-underline":1,"wysiwyg-font-size-xx-small":1,"wysiwyg-text-align-right":1,"wysiwyg-color-green":1,"wysiwyg-font-size-medium":1,"wysiwyg-float-right":1,"wysiwyg-font-size-larger":1,"wysiwyg-clear-both":1,"wysiwyg-text-align-left":1,"wysiwyg-color-red":1,"wysiwyg-color-black":1,"wysiwyg-color-maroon":1,"wysiwyg-color-fuchsia":1,"wysiwyg-color-blue":1,"wysiwyg-text-align-justify":1,"wysiwyg-color-navy":1,"wysiwyg-clear-left":1,"wysiwyg-color-xing":1,"wysiwyg-font-size-large":1,"wysiwyg-color-silver":1,"wysiwyg-color-aqua":1,"wysiwyg-color-gray":1,"wysiwyg-font-size-smaller":1,"wysiwyg-color-white":1,"wysiwyg-color-lime":1,"wysiwyg-text-align-center":1,"wysiwyg-font-size-xx-large":1,"wysiwyg-color-teal":1,"wysiwyg-float-left":1},"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":{"href":"url"},"set_attributes":{"rel":"nofollow","target":"_blank"}},"img":{"check_attributes":{"width":"numbers","alt":"alt","src":"url","height":"numbers"},"add_class":{"align":"align_img"}},"rb":{"rename_tag":"span"},"footer":{"rename_tag":"div"},"noframes":{"remove":1},"abbr":{"rename_tag":"span"},"u":{"rename_tag":"span","set_class":"wysiwyg-text-decoration-underline"},"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"}}}
View
30 examples/simple.html
@@ -87,41 +87,33 @@
<small>powered by <a href="https://github.com/xing/wysihtml5" target="_blank">wysihtml5</a>.</small>
-<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"></script>
-<script type="text/javascript" src="../dist/wysihtml5-0.1.0.js"></script>
+<script src="../parser_rules/simple.js"></script>
+<script src="../dist/wysihtml5-0.2.0.js"></script>
<script>
var editor = new wysihtml5.Editor("textarea", {
toolbar: "toolbar",
- parserRules: {
- tags: {
- br: true,
- strong: true,
- b: true,
- i: true,
- em: true,
- span: true,
- a: { check_attributes: { href: "url" }, set_attributes: { rel: "nofollow", target: "_blank" } }
- }
- }
+ parserRules: wysihtml5ParserRules
});
+ var log = document.getElementById("log");
+
editor
.observe("load", function() {
- $("log").insert("<div>load</div>");
+ log.innerHTML += "<div>load</div>";
})
.observe("focus", function() {
- $("log").insert("<div>focus</div>");
+ log.innerHTML += "<div>focus</div>";
})
.observe("blur", function() {
- $("log").insert("<div>blur</div>");
+ log.innerHTML += "<div>blur</div>";
})
.observe("change", function() {
- $("log").insert("<div>change</div>");
+ log.innerHTML += "<div>change</div>";
})
.observe("paste", function() {
- $("log").insert("<div>paste</div>");
+ log.innerHTML += "<div>paste</div>";
})
.observe("newword:composer", function() {
- $("log").insert("<div>newword:composer</div>");
+ log.innerHTML += "<div>newword:composer</div>";
});
</script>
View
139 lib/base/base.js
@@ -0,0 +1,139 @@
+/*
+ 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());
+ }
+});
View
461 lib/rangy/rangy-cssclassapplier-wysihtml5.js
@@ -1,461 +0,0 @@
-/**
- * @license CSS Class Applier module for Rangy.
- * Adds, removes and toggles CSS classes on Ranges and Selections
- *
- * Part of Rangy, a cross-browser JavaScript range and selection library
- * http://code.google.com/p/rangy/
- *
- * Depends on Rangy core.
- *
- * Copyright 2011, Tim Down
- * Licensed under the MIT license.
- * Version: 1.1
- * Build date: 12 March 2011
- *
- * Adjusted by Christopher Blum <christopher.blum@xing.com> to match WYSIHTML5 logic
- * in order to be able ...
- * - to use custom tags
- * - to detect and replace similar css classes via reg exp
- */
-rangy.createModule("CssClassApplier", function(api, module) {
- api.requireModules( ["WrappedSelection", "WrappedRange"] );
-
- var dom = api.dom;
-
- var defaultTagName = "span";
-
- function trim(str) {
- return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
- }
-
- function hasClass(el, cssClass, regExp) {
- if (!el.className) {
- return false;
- }
-
- var matchingClassNames = el.className.match(regExp) || [];
- return matchingClassNames[matchingClassNames.length - 1] === cssClass;
- }
-
- function addClass(el, cssClass, regExp) {
- if (el.className) {
- removeClass(el, regExp);
- el.className += " " + cssClass;
- } else {
- el.className = cssClass;
- }
- }
-
- function removeClass(el, regExp) {
- if (el.className) {
- el.className = el.className.replace(regExp, "");
- }
- }
-
- function hasSameClasses(el1, el2) {
- return el1.className.replace(/\s+/, " ") == el2.className.replace(/\s+/, " ");
- }
-
- function replaceWithOwnChildren(el) {
- var parent = el.parentNode;
- while (el.hasChildNodes()) {
- parent.insertBefore(el.firstChild, el);
- }
- parent.removeChild(el);
- }
-
- function elementsHaveSameNonClassAttributes(el1, el2) {
- if (el1.attributes.length != el2.attributes.length) return false;
- for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
- attr1 = el1.attributes[i];
- name = attr1.name;
- if (name != "class") {
- attr2 = el2.attributes.getNamedItem(name);
- if (attr1.specified != attr2.specified) return false;
- if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
- }
- }
- return true;
- }
-
- function isSplitPoint(node, offset) {
- if (dom.isCharacterDataNode(node)) {
- if (offset == 0) {
- return !!node.previousSibling;
- } else if (offset == node.length) {
- return !!node.nextSibling;
- } else {
- return true;
- }
- }
-
- return offset > 0 && offset < node.childNodes.length;
- }
-
- function splitNodeAt(node, descendantNode, descendantOffset) {
-
- var newNode;
- if (dom.isCharacterDataNode(descendantNode)) {
- if (descendantOffset == 0) {
- descendantOffset = dom.getNodeIndex(descendantNode);
- descendantNode = descendantNode.parentNode;
- } else if (descendantOffset == descendantNode.length) {
- descendantOffset = dom.getNodeIndex(descendantNode) + 1;
- descendantNode = descendantNode.parentNode;
- } else {
- newNode = dom.splitDataNode(descendantNode, descendantOffset);
- }
- }
- if (!newNode) {
- newNode = descendantNode.cloneNode(false);
- if (newNode.id) {
- newNode.removeAttribute("id");
- }
- var child;
- while ((child = descendantNode.childNodes[descendantOffset])) {
-
- newNode.appendChild(child);
- }
- dom.insertAfter(newNode, descendantNode);
- }
- return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode));
- }
-
- function Merge(firstNode) {
- this.isElementMerge = (firstNode.nodeType == 1);
- this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
- this.textNodes = [this.firstTextNode];
- }
-
- Merge.prototype = {
- doMerge: function() {
- var textBits = [], textNode, parent, text;
- for (var i = 0, len = this.textNodes.length; i < len; ++i) {
- textNode = this.textNodes[i];
- parent = textNode.parentNode;
- textBits[i] = textNode.data;
- if (i) {
- parent.removeChild(textNode);
- if (!parent.hasChildNodes()) {
- parent.parentNode.removeChild(parent);
- }
- }
- }
- this.firstTextNode.data = text = textBits.join("");
- return text;
- },
-
- getLength: function() {
- var i = this.textNodes.length, len = 0;
- while (i--) {
- len += this.textNodes[i].length;
- }
- return len;
- },
-
- toString: function() {
- var textBits = [];
- for (var i = 0, len = this.textNodes.length; i < len; ++i) {
- textBits[i] = "'" + this.textNodes[i].data + "'";
- }
- return "[Merge(" + textBits.join(",") + ")]";
- }
- };
-
- function CssClassApplier(tagNames, cssClass, similarClassRegExp, normalize) {
- this.tagNames = tagNames || [defaultTagName];
- this.cssClass = cssClass || "";
- this.similarClassRegExp = similarClassRegExp;
- this.normalize = normalize;
- this.applyToAnyTagName = false;
- }
-
- CssClassApplier.prototype = {
- getAncestorWithClass: function(node) {
- var cssClassMatch;
- while (node) {
- cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
- if (node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
- return node;
- }
- node = node.parentNode;
- }
- return false;
- },
-
- // Normalizes nodes after applying a CSS class to a Range.
- postApply: function(textNodes, range) {
- var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
-
- var merges = [], currentMerge;
-
- var rangeStartNode = firstNode, rangeEndNode = lastNode;
- var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
-
- var textNode, precedingTextNode;
-
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
- if (precedingTextNode) {
- if (!currentMerge) {
- currentMerge = new Merge(precedingTextNode);
- merges.push(currentMerge);
- }
- currentMerge.textNodes.push(textNode);
- if (textNode === firstNode) {
- rangeStartNode = currentMerge.firstTextNode;
- rangeStartOffset = rangeStartNode.length;
- }
- if (textNode === lastNode) {
- rangeEndNode = currentMerge.firstTextNode;
- rangeEndOffset = currentMerge.getLength();
- }
- } else {
- currentMerge = null;
- }
- }
-
- // Test whether the first node after the range needs merging
- var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
- if (nextTextNode) {
- if (!currentMerge) {
- currentMerge = new Merge(lastNode);
- merges.push(currentMerge);
- }
- currentMerge.textNodes.push(nextTextNode);
- }
-
- // Do the merges
- if (merges.length) {
- for (i = 0, len = merges.length; i < len; ++i) {
- merges[i].doMerge();
- }
- // Set the range boundaries
- range.setStart(rangeStartNode, rangeStartOffset);
- range.setEnd(rangeEndNode, rangeEndOffset);
- }
-
- },
-
- getAdjacentMergeableTextNode: function(node, forward) {
- var isTextNode = (node.nodeType == 3);
- var el = isTextNode ? node.parentNode : node;
- var adjacentNode;
- var propName = forward ? "nextSibling" : "previousSibling";
- if (isTextNode) {
- // Can merge if the node's previous/next sibling is a text node
- adjacentNode = node[propName];
- if (adjacentNode && adjacentNode.nodeType == 3) {
- return adjacentNode;
- }
- } else {
- // Compare element with its sibling
- adjacentNode = el[propName];
- if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
- return adjacentNode[forward ? "firstChild" : "lastChild"];
- }
- }
- return null;
- },
-
- areElementsMergeable: function(el1, el2) {
- return dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
- && dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
- && hasSameClasses(el1, el2)
- && elementsHaveSameNonClassAttributes(el1, el2);
- },
-
- createContainer: function(doc) {
- var el = doc.createElement(this.tagNames[0]);
- if (this.cssClass) {
- el.className = this.cssClass;
- }
- return el;
- },
-
- applyToTextNode: function(textNode) {
- var parent = textNode.parentNode;
- if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
- if (this.cssClass) {
- addClass(parent, this.cssClass, this.similarClassRegExp);
- }
- } else {
- var el = this.createContainer(dom.getDocument(textNode));
- textNode.parentNode.insertBefore(el, textNode);
- el.appendChild(textNode);
- }
- },
-
- isRemovable: function(el) {
- return dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && trim(el.className) == this.cssClass;
- },
-
- undoToTextNode: function(textNode, range, ancestorWithClass) {
- if (!range.containsNode(ancestorWithClass)) {
- // Split out the portion of the ancestor from which we can remove the CSS class
- var ancestorRange = range.cloneRange();
- ancestorRange.selectNode(ancestorWithClass);
-
- if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
- splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
- range.setEndAfter(ancestorWithClass);
- }
- if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
- ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
- }
- }
-
- if (this.similarClassRegExp) {
- removeClass(ancestorWithClass, this.similarClassRegExp);
- }
- if (this.isRemovable(ancestorWithClass)) {
- replaceWithOwnChildren(ancestorWithClass);
- }
- },
-
- applyToRange: function(range) {
- var textNodes = range.getNodes([3]);
- if (!textNodes.length) {
- try {
- var node = this.createContainer(range.endContainer.ownerDocument);
- range.surroundContents(node);
- this.selectNode(range, node);
- return;
- } catch(e) {}
- }
-
- range.splitBoundaries();
- textNodes = range.getNodes([3]);
-
- if (textNodes.length) {
- var textNode;
-
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- if (!this.getAncestorWithClass(textNode)) {
- this.applyToTextNode(textNode);
- }
- }
-
- range.setStart(textNodes[0], 0);
- textNode = textNodes[textNodes.length - 1];
- range.setEnd(textNode, textNode.length);
-
- if (this.normalize) {
- this.postApply(textNodes, range);
- }
- }
- },
-
- undoToRange: function(range) {
- var textNodes = range.getNodes([3]), textNode, ancestorWithClass;
- if (textNodes.length) {
- range.splitBoundaries();
- textNodes = range.getNodes([3]);
- } else {
- var doc = range.endContainer.ownerDocument,
- node = doc.createTextNode("\uFEFF");
- range.insertNode(node);
- range.selectNode(node);
- textNodes = [node];
- }
-
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- ancestorWithClass = this.getAncestorWithClass(textNode);
- if (ancestorWithClass) {
- this.undoToTextNode(textNode, range, ancestorWithClass);
- }
- }
-
- if (len == 1) {
- this.selectNode(range, textNodes[0]);
- } else {
- range.setStart(textNodes[0], 0);
- textNode = textNodes[textNodes.length - 1];
- range.setEnd(textNode, textNode.length);
-
- if (this.normalize) {
- this.postApply(textNodes, range);
- }
- }
- },
-
- selectNode: function(range, node) {
- var isElement = node.nodeType === 1,
- canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true,
- content = isElement ? node.innerHTML : node.data,
- isEmpty = (content === "" || content === "\uFEFF");
-
- if (isEmpty && isElement && canHaveHTML) {
- // Make sure that caret is visible in node by inserted a zero width no breaking space
- try { node.innerHTML = "\uFEFF"; } catch(e) {}
- }
- range.selectNodeContents(node);
- if (isEmpty && isElement) {
- range.collapse(false);
- } else if (isEmpty) {
- range.setStartAfter(node);
- range.setEndAfter(node);
- }
- },
-
- getTextSelectedByRange: function(textNode, range) {
- var textRange = range.cloneRange();
- textRange.selectNodeContents(textNode);
-
- var intersectionRange = textRange.intersection(range);
- var text = intersectionRange ? intersectionRange.toString() : "";
- textRange.detach();
-
- return text;
- },
-
- isAppliedToRange: function(range) {
- var ancestors = [],
- ancestor,
- textNodes = range.getNodes([3]);
- if (!textNodes.length) {
- var ancestor = this.getAncestorWithClass(range.startContainer);
- return ancestor ? [ancestor] : false;
- }
-
- for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
- selectedText = this.getTextSelectedByRange(textNodes[i], range);
- ancestor = this.getAncestorWithClass(textNodes[i]);
- if (selectedText != "" && !ancestor) {
- return false;
- } else {
- ancestors.push(ancestor);
- }
- }
- return ancestors;
- },
-
- toggleRange: function(range) {
- if (this.isAppliedToRange(range)) {
- this.undoToRange(range);
- } else {
- this.applyToRange(range);
- }
- }
- };
-
- function createCssClassApplier(tagNames, cssClass, classRegExp, normalize) {
- return new CssClassApplier(tagNames, cssClass, classRegExp, normalize);
- }
-
- CssClassApplier.util = {
- hasClass: hasClass,
- addClass: addClass,
- removeClass: removeClass,
- hasSameClasses: hasSameClasses,
- replaceWithOwnChildren: replaceWithOwnChildren,
- elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
- splitNodeAt: splitNodeAt
- };
-
- api.CssClassApplier = CssClassApplier;
- api.createCssClassApplier = createCssClassApplier;
-});
View
563 parser_rules/advanced.js
@@ -0,0 +1,563 @@
+/**
+ * Full HTML5 compatibility rule set
+ * Creates fully valid, secure and compatible HTML5 without affecting the visual appearance
+ *
+ * Examples:
+ * <font size="1">foo</font>
+ * ... becomes ...
+ * <span class="wysiwyg-font-size-xx-small">foo</span>
+ *
+ * <a href="http://foobar.com">foo</a>
+ * ... becomes ...
+ * <a href="http://foobar.com" target="_blank" rel="nofollow">foo</a>
+ *
+ * <img align="left" src="http://foobar.com/image.png">
+ * ... becomes ...
+ * <img class="wysiwyg-float-left" src="http://foobar.com/image.png" alt="">
+ *
+ * <u>foo</u>
+ * ... becomes ...
+ * <span class="wysiwyg-text-decoration-underline">foo</span>
+ *
+ * <div>foo<script>alert(document.cookie)</script></div>
+ * ... becomes ...
+ * <div>foo</div>
+ *
+ * <marquee>foo</marquee>
+ * ... becomes ...
+ * <span>foo</marquee>
+ *
+ * foo <br clear="both"> bar
+ * ... becomes ...
+ * foo <br class="wysiwyg-clear-both"> bar
+ *
+ * <div>hello <iframe src="http://google.com"></iframe></div>
+ * ... becomes ...
+ * <div>hello </div>
+ *
+ * <center>hello</center>
+ * ... becomes ...
+ * <div class="wysiwyg-text-align-center">hello</div>
+ */
+var wysihtml5ParserRules = {
+ /**
+ * CSS Class white-list
+ * Following css classes won't be removed when parsed by the wysihtml5 html parser
+ */
+ "classes": {
+ "wysiwyg-font-size-x-large": 1,
+ "wysiwyg-font-size-small": 1,
+ "wysiwyg-color-yellow": 1,
+ "wysiwyg-color-purple": 1,
+ "wysiwyg-font-size-x-small": 1,
+ "wysiwyg-color-olive": 1,
+ "wysiwyg-clear-right": 1,
+ "wysiwyg-text-decoration-underline": 1,
+ "wysiwyg-font-size-xx-small": 1,
+ "wysiwyg-text-align-right": 1,
+ "wysiwyg-color-green": 1,
+ "wysiwyg-font-size-medium": 1,
+ "wysiwyg-float-right": 1,
+ "wysiwyg-font-size-larger": 1,
+ "wysiwyg-clear-both": 1,
+ "wysiwyg-text-align-left": 1,
+ "wysiwyg-color-red": 1,
+ "wysiwyg-color-black": 1,
+ "wysiwyg-color-maroon": 1,
+ "wysiwyg-color-fuchsia": 1,
+ "wysiwyg-color-blue": 1,
+ "wysiwyg-text-align-justify": 1,
+ "wysiwyg-color-navy": 1,
+ "wysiwyg-clear-left": 1,
+ "wysiwyg-color-xing": 1,
+ "wysiwyg-font-size-large": 1,
+ "wysiwyg-color-silver": 1,
+ "wysiwyg-color-aqua": 1,
+ "wysiwyg-color-gray": 1,
+ "wysiwyg-font-size-smaller": 1,
+ "wysiwyg-color-white": 1,
+ "wysiwyg-color-lime": 1,
+ "wysiwyg-text-align-center": 1,
+ "wysiwyg-font-size-xx-large": 1,
+ "wysiwyg-color-teal": 1,
+ "wysiwyg-float-left": 1
+ },
+ /**
+ * Tag list
+ *
+ * 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-*")
+ <p align="center">foo</p> ... becomes ... <p> class="wysiwyg-text-align-center">foo</p>
+ * - clear_br: converts clear attribute values left/right/all/both to their corresponding css class "wysiwyg-clear-*"
+ * <br clear="all"> ... becomes ... <br class="wysiwyg-clear-both">
+ * - align_img: converts align attribute values (right/left) on <img> to their corresponding css class "wysiwyg-float-*"
+ *
+ * - remove: removes the element and it's 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: checks whether the given string is an url, deletes the attribute if not
+ * - 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
+ */
+ "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": {
+ "href": "url"
+ },
+ "set_attributes": {
+ "rel": "nofollow",
+ "target": "_blank"
+ }
+ },
+ "img": {
+ "check_attributes": {
+ "width": "numbers",
+ "alt": "alt",
+ "src": "url",
+ "height": "numbers"
+ },
+ "add_class": {
+ "align": "align_img"
+ }
+ },
+ "rb": {
+ "rename_tag": "span"
+ },
+ "footer": {
+ "rename_tag": "div"
+ },
+ "noframes": {
+ "remove": 1
+ },
+ "abbr": {
+ "rename_tag": "span"
+ },
+ "u": {
+ "rename_tag": "span",
+ "set_class": "wysiwyg-text-decoration-underline"
+ },
+ "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"
+ }
+ }
+};
View
32 parser_rules/simple.js
@@ -0,0 +1,32 @@
+/**
+ * Very simple basic rule set
+ *
+ * Allows
+ * <i>, <em>, <b>, <strong>, <p>, <div>, <a href="http://foo"></a>, <br>, <span>, <ol>, <ul>, <li>
+ *
+ * For a proper documentation of the format check full.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
+ }
+ }
+ }
+};
View
18 src/assert/html_equals.js
@@ -57,7 +57,7 @@ wysihtml5.assert.htmlEquals = (function() {
})();
/**
- * As stated above, firefox doesn't preserve original attribute order
+ * 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.
@@ -97,15 +97,15 @@ wysihtml5.assert.htmlEquals = (function() {
var removeWhiteSpace = (function() {
var REG_EXP = /(>)(\s*?)(<)/gm;
return function(html) {
- return html.replace(REG_EXP, "$1$3").strip();
+ 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.utils.getInDomElement(actual).innerHTML;
- expected = wysihtml5.utils.getInDomElement(expected).innerHTML;
+ actual = wysihtml5.dom.getAsDom(actual).innerHTML;
+ expected = wysihtml5.dom.getAsDom(expected).innerHTML;
}
if (config.normalizeWhiteSpace || DOESNT_PRESERVE_WHITE_SPACE) {
@@ -118,12 +118,8 @@ wysihtml5.assert.htmlEquals = (function() {
expected = removeWhiteSpace(expected);
}
- if (REORDERS_ATTRIBUTES) {
- actual = tokenizeHTML(actual);
- expected = tokenizeHTML(expected);
- ok(actual == expected, message);
- return;
- }
- equals(actual, expected, message);
+ actual = tokenizeHTML(actual);
+ expected = tokenizeHTML(expected);
+ ok(actual == expected, message);
};
})();
View
318 src/browser.js
@@ -0,0 +1,318 @@
+/**
+ * Detect browser support for specific features
+ *
+ * @author Christopher Blum <christopher.blum@xing.com>
+ */
+wysihtml5.browser = (function() {
+ var userAgent = navigator.userAgent,
+ testElement = document.createElement("div"),
+ // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
+ isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1,
+ isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,
+ isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,
+ isOpera = userAgent.indexOf("Opera/") !== -1;
+
+ 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)
+ isIncompatibleMobileBrowser = (userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1) || userAgent.indexOf("opera mobi") !== -1;
+
+ return hasContentEditableSupport
+ && hasEditingApiSupport
+ && hasQuerySelectorSupport
+ && !isIncompatibleMobileBrowser;
+ },
+
+ /**
+ * Whether the browser supports sandboxed iframes
+ * Currently only IE 6+ offers such feature <iframe security="restricted">
+ *
+ * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
+ * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
+ *
+ * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
+ */
+ supportsSandboxedIframes: function() {
+ return isIE;
+ },
+
+ /**
+ * IE6+7 throw a mixed content warning when the src of an iframe
+ * is empty/unset or about:blank
+ * window.querySelector is implemented as of IE8
+ */
+ throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
+ return !("querySelector" in document);
+ },
+
+ /**
+ * Whether the caret is correctly displayed in contentEditable elements
+ * Firefox sometimes shows a huge caret in the beginning after focusing
+ */
+ displaysCaretInEmptyContentEditableCorrectly: function() {
+ return !isGecko;
+ },
+
+ /**
+ * Opera and IE are the only browsers who offer the css value
+ * in the original unit, thx to the currentStyle object
+ * All other browsers provide the computed style in px via window.getComputedStyle
+ */
+ hasCurrentStyleProperty: function() {
+ return "currentStyle" in testElement;
+ },
+
+ /**
+ * Whether the browser inserts a <br> when pressing enter in a contentEditable element
+ */
+ insertsLineBreaksOnReturn: function() {
+ return isGecko;
+ },
+
+ supportsPlaceholderAttributeOn: function(element) {
+ return "placeholder" in element;
+ },
+
+ supportsEvent: function(eventName) {
+ return "on" + eventName in testElement || (function() {
+ testElement.setAttribute("on" + eventName, "return;");
+ return typeof(testElement["on" + eventName]) === "function";
+ })();
+ },
+
+ /**
+ * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
+ */
+ supportsEventsInIframeCorrectly: function() {
+ return !isOpera;
+ },
+
+ /**
+ * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled
+ * with event.preventDefault
+ * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs
+ * to be cancelled
+ */
+ firesOnDropOnlyWhenOnDragOverIsCancelled: function() {
+ return isWebKit || isGecko;
+ },
+
+ /**
+ * Whether the browser supports the event.dataTransfer property in a proper way
+ */
+ supportsDataTransfer: function() {
+ try {
+ // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)
+ return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;
+ } catch(e) {
+ return false;
+ }
+ },
+
+ /**
+ * Everything below IE9 doesn't know how to treat HTML5 tags
+ *
+ * @param {Object} context The document object on which to check HTML5 support
+ *
+ * @example
+ * wysihtml5.browser.supportsHTML5Tags(document);
+ */
+ supportsHTML5Tags: function(context) {
+ var element = context.createElement("div"),
+ html5 = "<article>foo</article>";
+ element.innerHTML = html5;
+ return element.innerHTML.toLowerCase() === html5;
+ },
+
+ /**
+ * Checks whether a document supports a certain queryCommand
+ * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
+ * in oder to report correct results
+ *
+ * @param {Object} doc Document object on which to check for a query command
+ * @param {String} command The query command to check for
+ * @return {Boolean}
+ *
+ * @example
+ * wysihtml5.browser.supportsCommand(document, "bold");
+ */
+ supportsCommand: (function() {
+ // Following commands are supported but contain bugs in some browsers
+ var buggyCommands = {
+ // formatBlock fails with some tags (eg. <blockquote>)
+ "formatBlock": isIE,
+ // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
+ // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
+ // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
+ "insertUnorderedList": isIE || isOpera,
+ "insertOrderedList": isIE || isOpera
+ };
+
+ return function(doc, command) {
+ var isBuggy = buggyCommands[command];
+ if (!isBuggy) {
+ // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
+ try {
+ return doc.queryCommandSupported(command);
+ } catch(e1) {}
+
+ try {
+ return doc.queryCommandEnabled(command);
+ } catch(e2) {}
+ }
+ return false;
+ };
+ })(),
+
+ /**
+ * IE: URLs starting with:
+ * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
+ * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
+ * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
+ * space bar when the caret is directly after such an url.
+ * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
+ * (related blog post on msdn
+ * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
+ */
+ doesAutoLinkingInContentEditable: function() {
+ return isIE;
+ },
+
+ /**
+ * As stated above, IE auto links urls typed into contentEditable elements
+ * Since IE9 it's possible to prevent this behavior
+ */
+ canDisableAutoLinking: function() {
+ return this.supportsCommand(document, "AutoUrlDetect");
+ },
+
+ /**
+ * IE leaves an empty paragraph in the contentEditable element after clearing it
+ * Chrome/Safari sometimes an empty <div>
+ */
+ clearsContentEditableCorrectly: function() {
+ return isGecko || isOpera || isWebKit;
+ },
+
+ /**
+ * IE gives wrong results for getAttribute
+ */
+ supportsGetAttributeCorrectly: function() {
+ var td = document.createElement("td");
+ return td.getAttribute("rowspan") != "1";
+ },
+
+ /**
+ * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
+ * Chrome and Safari both don't support this
+ */
+ canSelectImagesInContentEditable: function() {
+ return isGecko || isIE || isOpera;
+ },
+
+ /**
+ * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container
+ * pressing backspace doesn't remove the entire list as done in other browsers
+ */
+ clearsListsInContentEditableCorrectly: function() {
+ return isGecko || isIE || isWebKit;
+ },
+
+ /**
+ * All browsers except Safari and Chrome automatically scroll the range/caret position into view
+ */
+ autoScrollsToCaret: function() {
+ return !isWebKit;
+ },
+
+ /**
+ * Check whether the browser automatically closes tags that don't need to be opened
+ */
+ autoClosesUnclosedTags: function() {
+ var clonedTestElement = testElement.cloneNode(false),
+ returnValue,
+ innerHTML;
+
+ clonedTestElement.innerHTML = "<p><div></div>";
+ innerHTML = clonedTestElement.innerHTML.toLowerCase();
+ returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
+
+ // Cache result by overwriting current function
+ this.autoClosesUnclosedTags = function() { return returnValue; };
+
+ return returnValue;
+ },
+
+ /**
+ * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
+ */
+ supportsNativeGetElementsByClassName: function() {
+ return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
+ },
+
+ /**
+ * As of now (19.04.2011) only supported by Firefox 4 and Chrome
+ * See https://developer.mozilla.org/en/DOM/Selection/modify
+ */
+ supportsSelectionModify: function() {
+ return "getSelection" in window && "modify" in window.getSelection();
+ },
+
+ /**
+ * Whether the browser supports the classList object for fast className manipulation
+ * See https://developer.mozilla.org/en/DOM/element.classList
+ */
+ supportsClassList: function() {
+ return "classList" in testElement;
+ },
+
+ /**
+ * Opera needs a white space after a <br> in order to position the caret correctly
+ */
+ needsSpaceAfterLineBreak: function() {
+ return isOpera;
+ },
+
+ /**
+ * Whether the browser supports the speech api on the given element
+ * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
+ *
+ * @example
+ * var input = document.createElement("input");
+ * if (wysihtml5.browser.supportsSpeechApiOn(input)) {
+ * // ...
+ * }
+ */
+ supportsSpeechApiOn: function(input) {
+ var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0];
+ return chromeVersion[1<