mercury-rails 0.2.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/POST_INSTALL +15 -0
  2. data/README.md +27 -6
  3. data/VERSION +1 -1
  4. data/app/controllers/mercury_controller.rb +4 -4
  5. data/app/views/layouts/mercury.html.erb +14 -3
  6. data/app/views/mercury/panels/snippets.html +1 -1
  7. data/app/views/mercury/snippets/{example_options.html.erb → example/options.html.erb} +0 -0
  8. data/app/views/mercury/snippets/{example.html.erb → example/preview.html.erb} +0 -0
  9. data/config/routes.rb +2 -2
  10. data/features/loading/loading.feature +22 -0
  11. data/features/loading/navigating.feature +77 -0
  12. data/features/loading/user_interface.feature +67 -0
  13. data/features/regions/editable/advanced_editing.feature +0 -0
  14. data/features/regions/editable/basic_editing.feature +195 -0
  15. data/features/regions/editable/inserting_links.feature +98 -0
  16. data/features/regions/editable/inserting_media.feature +110 -0
  17. data/features/regions/editable/inserting_snippets.feature +103 -0
  18. data/features/regions/editable/inserting_special_characters.feature +24 -0
  19. data/features/regions/editable/inserting_tables.feature +109 -0
  20. data/features/regions/editable/pasting.feature +0 -0
  21. data/features/regions/editable/uploading_images.feature +0 -0
  22. data/features/regions/markupable/advanced_editing.feature +0 -0
  23. data/features/regions/markupable/basic_editing.feature +0 -0
  24. data/features/regions/markupable/inserting_links.feature +0 -0
  25. data/features/regions/markupable/inserting_media.feature +0 -0
  26. data/features/regions/markupable/inserting_snippets.feature +0 -0
  27. data/features/regions/markupable/inserting_special_characters.feature +0 -0
  28. data/features/regions/markupable/inserting_tables.feature +0 -0
  29. data/features/regions/markupable/uploading_images.feature +0 -0
  30. data/features/regions/snippetable/advanced_editing.feature +0 -0
  31. data/features/regions/snippetable/basic_editing.feature +0 -0
  32. data/features/regions/snippetable/inserting_snippets.feature +0 -0
  33. data/features/saving/saving.feature +33 -0
  34. data/features/step_definitions/debug_steps.rb +2 -2
  35. data/features/step_definitions/mercury_steps.rb +441 -0
  36. data/features/support/env.rb +3 -3
  37. data/features/support/mercury_contents.rb +25 -0
  38. data/features/support/mercury_selectors.rb +147 -0
  39. data/features/support/paths.rb +20 -18
  40. data/features/support/selectors.rb +5 -3
  41. data/lib/generators/mercury/install/install_generator.rb +14 -0
  42. data/mercury-rails.gemspec +50 -20
  43. data/spec/javascripts/mercury/lightview_spec.js.coffee +55 -27
  44. data/spec/javascripts/mercury/mercury_spec.js.coffee +3 -3
  45. data/spec/javascripts/mercury/modal_spec.js.coffee +2 -2
  46. data/spec/javascripts/mercury/native_extensions_spec.js.coffee +0 -24
  47. data/spec/javascripts/mercury/page_editor_spec.js.coffee +148 -67
  48. data/spec/javascripts/mercury/panel_spec.js.coffee +2 -2
  49. data/spec/javascripts/mercury/region_spec.js.coffee +10 -7
  50. data/spec/javascripts/mercury/regions/editable_spec.js.coffee +0 -20
  51. data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +2 -2
  52. data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +1 -1
  53. data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +1 -1
  54. data/spec/javascripts/templates/mercury/page_editor.html +3 -3
  55. data/vendor/assets/images/mercury/close.png +0 -0
  56. data/vendor/assets/javascripts/mercury.js +140 -73
  57. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-1.6.js +0 -0
  58. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-ui-1.8.13.custom.js +0 -0
  59. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery.additions.js +0 -0
  60. data/vendor/assets/javascripts/mercury/dependencies/jquery.htmlClean.js +527 -0
  61. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/liquidmetal.js +0 -0
  62. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/showdown.js +0 -0
  63. data/vendor/assets/javascripts/mercury/lightview.js.coffee +5 -2
  64. data/vendor/assets/javascripts/mercury/mercury.js.coffee +9 -8
  65. data/vendor/assets/javascripts/mercury/modals/htmleditor.js.coffee +3 -1
  66. data/vendor/assets/javascripts/mercury/modals/inserttable.js.coffee +2 -2
  67. data/vendor/assets/javascripts/mercury/native_extensions.js.coffee +6 -17
  68. data/vendor/assets/javascripts/mercury/page_editor.js.coffee +29 -8
  69. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/mercury/page_editor.js.coffee +27 -0
  70. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/plugin.js +9 -0
  71. data/vendor/assets/javascripts/mercury/region.js.coffee +2 -2
  72. data/vendor/assets/javascripts/mercury/regions/editable.js.coffee +89 -93
  73. data/vendor/assets/javascripts/mercury/regions/markupable.js.coffee +1 -1
  74. data/vendor/assets/javascripts/mercury/support/history.js +1 -0
  75. data/vendor/assets/javascripts/mercury/uploader.js.coffee +0 -1
  76. data/vendor/assets/javascripts/mercury_loader.js +4 -4
  77. data/vendor/assets/stylesheets/mercury/lightview.css +8 -0
  78. data/vendor/assets/stylesheets/mercury/mercury.css +12 -0
  79. data/vendor/assets/stylesheets/mercury/modal.css +0 -12
  80. data/vendor/assets/stylesheets/mercury/toolbar.css +1 -0
  81. data/vendor/assets/stylesheets/mercury_overrides.css +17 -0
  82. metadata +73 -45
  83. data/app/views/mercury/lightviews/imageprocessor.html +0 -3
  84. data/app/views/mercury/modals/sanitizer.html +0 -9
  85. data/features/editing/basic.feature +0 -11
  86. data/vendor/assets/images/mercury/clippy.png +0 -0
@@ -10,7 +10,7 @@ describe "Mercury.Toolbar.Expander", ->
10
10
  afterEach ->
11
11
  @expander = null
12
12
  delete(@expander)
13
- $(document).unbind('mercury:resize')
13
+ $(window).unbind('mercury:resize')
14
14
 
15
15
  describe "constructor", ->
16
16
 
@@ -7,9 +7,9 @@
7
7
  <meta content="K6JhyfOVKJX8X2ZkiJXSf491fc1fF+k79wzrChHQa0g=" name="csrf-token" />
8
8
 
9
9
  <div id="page_editor_container"></div>
10
- <div id="region1" class="mercury-region" data-type="editable"></div>
11
- <div id="region2" class="mercury-region" data-type="editable"></div>
12
- <div id="region3" class="mercury-region" data-type="editable">
10
+ <div id="region1" class="custom-region-class" data-type="editable"></div>
11
+ <div id="region2" class="custom-region-class" data-type="editable"></div>
12
+ <div id="region3" class="custom-region-class" data-type="editable">
13
13
  <a id="anchor1r" href="#" target="_top">1</a>
14
14
  <a id="anchor2r" href="#" target="_blank">2</a>
15
15
  <a id="anchor3r" href="#" target="_self">3</a>
@@ -23,76 +23,20 @@
23
23
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
24
  *
25
25
  *= require_self
26
+ *
27
+ * Add all requires for the support libraries that integrate nicely with Mercury Editor.
28
+ * require mercury/support/history
29
+ *
30
+ * Require Mercury Editor itself.
26
31
  *= require mercury/mercury
32
+ *
33
+ * Add all requires for plugins that extend or change the behavior of Mercury Editor.
34
+ * require mercury/plugins/save_as_xml/plugin.js
27
35
  */
28
36
  window.MercurySetup = {
29
37
 
30
38
  // # Mercury Configuration
31
39
  config: {
32
- // ## Hijacking Links & Forms
33
- //
34
- // Mercury will hijack links and forms that don't have a target set, or the target is set to _self and will set it
35
- // to _top. This is because the target must be set properly for Mercury to not get in the way of some
36
- // functionality, like proper page loads on form submissions etc. Mercury doesn't do this to links or forms that
37
- // are within editable regions because it doesn't want to impact the html that's saved. With that being explained,
38
- // you can add classes to links or forms that you don't want this behavior added to. Let's say you have links that
39
- // open a lightbox style window, and you don't want the targets of these to be set to _top. You can add classes to
40
- // this array, and they will be ignored when the hijacking is applied.
41
- nonHijackableClasses: [],
42
-
43
-
44
- // ## Ajax and CSRF Headers
45
- //
46
- // Some server frameworks require that you provide a specific header for Ajax requests. The values for these CSRF
47
- // tokens are typically stored in the rendered DOM. By default, Mercury will look for the Rails specific meta tag,
48
- // and provide the X-CSRF-Token header on Ajax requests, but you can modify this configuration if the system you're
49
- // using doesn't follow the same standard.
50
- csrfSelector: 'meta[name="csrf-token"]',
51
- csrfHeader: 'X-CSRF-Token',
52
-
53
-
54
- // ## Pasting (in Chrome/Safari)
55
- //
56
- // When copying content using webkit, it embeds all the user defined styles (from the css files) into the html
57
- // style attributes directly. When pasting this content into HTML5 contentEditable elements it leaves these
58
- // intact. This can be a desired feature, or an annoyance, so you can enable it or disable it here. Keep in mind
59
- // this will only change the behavior in webkit, as mozilla doesn't do this.
60
- cleanStylesOnPaste: true,
61
-
62
-
63
- // ## Snippet Options and Preview
64
- //
65
- // When a user drags a snippet onto the page they'll be prompted to enter options for the given snippet. The server
66
- // is expected to respond with a form. Once the user submits this form, an Ajax request is sent to the server with
67
- // the options provided; this preview request is expected to respond with the rendered markup for the snippet.
68
- //
69
- // Name will be replaced with the snippet name (eg. example)
70
- snippets: {
71
- method: 'POST',
72
- optionsUrl: '/mercury/snippets/:name/options.html',
73
- previewUrl: '/mercury/snippets/:name/preview.html'
74
- },
75
-
76
-
77
- // ## Image Uploading
78
- //
79
- // If you drag images (while pressing shift) from your desktop into regions that support it, it will be uploaded
80
- // to the server and inserted into the region. This configuration allows you to specify if you want to
81
- // disable/enable this feature, the accepted mime-types, file size restrictions, and other things related to
82
- // uploading. You can optionally provide a handler function that takes the response from the server and returns an
83
- // object: {image: {url: '[your provided url]'}
84
- //
85
- // **Note:** Image uploading is only supported in some region types.
86
- uploading: {
87
- enabled: true,
88
- allowedMimeTypes: ['image/jpeg', 'image/gif', 'image/png'],
89
- maxFileSize: 1235242880,
90
- inputName: 'image[image]',
91
- url: '/images',
92
- handler: false
93
- },
94
-
95
-
96
40
  // ## Toolbars
97
41
  //
98
42
  // This is where you can customize the toolbars by adding or removing buttons, or changing them and their
@@ -247,17 +191,127 @@ window.MercurySetup = {
247
191
  },
248
192
 
249
193
 
194
+ // ## Hijacking Links & Forms
195
+ //
196
+ // Mercury will hijack links and forms that don't have a target set, or the target is set to _self and will set it
197
+ // to _top. This is because the target must be set properly for Mercury to not get in the way of some
198
+ // functionality, like proper page loads on form submissions etc. Mercury doesn't do this to links or forms that
199
+ // are within editable regions because it doesn't want to impact the html that's saved. With that being explained,
200
+ // you can add classes to links or forms that you don't want this behavior added to. Let's say you have links that
201
+ // open a lightbox style window, and you don't want the targets of these to be set to _top. You can add classes to
202
+ // this array, and they will be ignored when the hijacking is applied.
203
+ nonHijackableClasses: [],
204
+
205
+
206
+ // ## Ajax and CSRF Headers
207
+ //
208
+ // Some server frameworks require that you provide a specific header for Ajax requests. The values for these CSRF
209
+ // tokens are typically stored in the rendered DOM. By default, Mercury will look for the Rails specific meta tag,
210
+ // and provide the X-CSRF-Token header on Ajax requests, but you can modify this configuration if the system you're
211
+ // using doesn't follow the same standard.
212
+ csrfSelector: 'meta[name="csrf-token"]',
213
+ csrfHeader: 'X-CSRF-Token',
214
+
215
+
216
+ // ## Pasting & Sanitizing
217
+ //
218
+ // When pasting content into Mercury it may sometimes contain HTML tags and attributes. This markup is used to
219
+ // style the content and makes the pasted content look (and behave) the same as the original content. This can be a
220
+ // desired feature or an annoyance, so you can enable various sanitizing methods to clean the content when it's
221
+ // pasted.
222
+ //
223
+ // ### Sanitizing options:
224
+ // - false: no sanitizing is done, the content is pasted the exact same as it was copied by the user
225
+ // - 'whitelist': content is cleaned using the settings specified in the tag white list (described below)
226
+ // - 'text': all html is stripped before pasting, leaving only the raw text
227
+ //
228
+ // ### Using the whitelist configuration
229
+ //
230
+ // The white list allows you to specify tags and attributes that are allowed when pasting content. Each item in
231
+ // this object should contain the allowed tag, and an array of attributes that are allowed on that tag. If the
232
+ // allowed attributes array is empty, all attributes will be removed. If a tag is not present in this list, it will
233
+ // be removed, but without removing any of the text or tags inside it.
234
+ //
235
+ // **Note:** Content is *always* sanitized if looks like it's from MS Word or similar editors regardless of this
236
+ // configuration.
237
+ pasting: {
238
+ sanitize: 'whitelist',
239
+ whitelist: {
240
+ h1: [],
241
+ h2: [],
242
+ h3: [],
243
+ h4: [],
244
+ h5: [],
245
+ h6: [],
246
+ table: [],
247
+ thead: [],
248
+ tbody: [],
249
+ tfoot: [],
250
+ tr: [],
251
+ th: ['colspan', 'rowspan'],
252
+ td: ['colspan', 'rowspan'],
253
+ div: ['class'],
254
+ span: ['class'],
255
+ b: [],
256
+ bold: [],
257
+ i: [],
258
+ em: [],
259
+ u: [],
260
+ strike: [],
261
+ br: [],
262
+ p: [],
263
+ hr: [],
264
+ a: ['href', 'target', 'title', 'name'],
265
+ img: ['src', 'title', 'alt']
266
+ }
267
+ },
268
+
269
+
270
+ // ## Snippet Options and Preview
271
+ //
272
+ // When a user drags a snippet onto the page they'll be prompted to enter options for the given snippet. The server
273
+ // is expected to respond with a form. Once the user submits this form, an Ajax request is sent to the server with
274
+ // the options provided; this preview request is expected to respond with the rendered markup for the snippet.
275
+ //
276
+ // Name will be replaced with the snippet name (eg. example)
277
+ snippets: {
278
+ method: 'POST',
279
+ optionsUrl: '/mercury/snippets/:name/options.html',
280
+ previewUrl: '/mercury/snippets/:name/preview.html'
281
+ },
282
+
283
+
284
+ // ## Image Uploading
285
+ //
286
+ // If you drag images (while pressing shift) from your desktop into regions that support it, it will be uploaded
287
+ // to the server and inserted into the region. This configuration allows you to specify if you want to
288
+ // disable/enable this feature, the accepted mime-types, file size restrictions, and other things related to
289
+ // uploading. You can optionally provide a handler function that takes the response from the server and returns an
290
+ // object: {image: {url: '[your provided url]'}
291
+ //
292
+ // **Note:** Image uploading is only supported in some region types, and some browsers.
293
+ uploading: {
294
+ enabled: true,
295
+ allowedMimeTypes: ['image/jpeg', 'image/gif', 'image/png'],
296
+ maxFileSize: 1235242880,
297
+ inputName: 'image[image]',
298
+ url: '/images',
299
+ handler: false
300
+ },
301
+
302
+
250
303
  // ## Behaviors
251
304
  //
252
305
  // Behaviors are used to change the default behaviors of a given region type when a given button is clicked. For
253
306
  // example, you may prefer to add HR tags using an HR wrapped within a div with a classname (for styling). You
254
307
  // can add your own complex behaviors here.
255
308
  //
256
- // You can see how the behavior matches up directly with the button name. It's also important to note that the
309
+ // You can see how the behavior matches up directly with the button names. It's also important to note that the
257
310
  // callback functions are executed within the scope of the given region, so you have access to all it's methods.
311
+ // Here's some examples to help you get started.
258
312
  behaviors: {
259
- horizontalRule: function(selection) { selection.replace('<hr/>') },
260
- htmlEditor: function() { Mercury.modal('/mercury/modals/htmleditor.html', { title: 'HTML Editor', fullHeight: true, handler: 'htmlEditor' }) }
313
+ //foreColor: function(selection, options) { selection.wrap('<span style="color:' + options.value.toHex() + '">', true) },
314
+ htmlEditor: function() { Mercury.modal('/mercury/modals/htmleditor.html', { title: 'HTML Editor', fullHeight: true, handler: 'htmlEditor' }); }
261
315
  },
262
316
 
263
317
 
@@ -271,19 +325,32 @@ window.MercurySetup = {
271
325
  // and
272
326
  // Mercury.Toolbar.ButtonGroup.contexts
273
327
 
328
+
329
+ // ## Region Class
330
+ //
331
+ // Mercury identifies editable regions by a region class. This class has to be added in your HTML in advance, and
332
+ // is the only real Mercury code/naming exposed in the implementation of Mercury. To allow this to be as
333
+ // configurable as possible, you can set the name of the class here. When switching to preview mode, this
334
+ // configuration is used to generate a class to indicate that Mercury is in preview mode -- which will be this
335
+ // class with '-preview' appended (so, mercury-region-preview by default)
336
+ regionClass: 'mercury-region',
274
337
 
338
+
275
339
  // ## Styles
276
340
  //
277
341
  // Mercury tries to stay as much out of your code as possible, but because regions appear within your document we
278
342
  // need to include a few styles to indicate regions, as well as the different states of them (eg. focused). These
279
343
  // styles are injected into your document, and as simple as they might be, you may want to change them. You can do
280
- // so here.
344
+ // so here. {{regionClass}} will be automatically replaced with whatever you have set in the regionClass
345
+ // configuration directive.
281
346
  injectedStyles: '' +
282
- '.mercury-region, .mercury-textarea { min-height: 10px; outline: 1px dotted #09F }' +
347
+ '.{{regionClass}} { min-height: 10px; outline: 1px dotted #09F } ' +
348
+ '.{{regionClass}}:focus, .{{regionClass}}.focus { outline: none; -webkit-box-shadow: 0 0 10px #09F, 0 0 1px #045; box-shadow: 0 0 10px #09F, 0 0 1px #045 }' +
349
+ '.{{regionClass}}:after { content: "."; display: block; visibility: hidden; clear: both; height: 0; overflow: hidden; }' +
350
+ '.{{regionClass}} table, .{{regionClass}} td, .{{regionClass}} th { border: 1px dotted red; }' +
283
351
  '.mercury-textarea { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; resize: vertical; }' +
284
- '.mercury-region:focus, .mercury-region.focus, .mercury-textarea.focus { outline: none; -webkit-box-shadow: 0 0 10px #09F, 0 0 1px #045; box-shadow: 0 0 10px #09F, 0 0 1px #045 }' +
285
- '.mercury-region:after { content: "."; display: block; visibility: hidden; clear: both; height: 0; overflow: hidden; }' +
286
- '.mercury-region table, .mercury-region td, .mercury-region th { border: 1px dotted red; }'
352
+ '.mercury-textarea { min-height: 10px; outline: 1px dotted #09F }' +
353
+ '.mercury-textarea:focus, .mercury-textarea.focus { outline: none; -webkit-box-shadow: 0 0 10px #09F, 0 0 1px #045; box-shadow: 0 0 10px #09F, 0 0 1px #045 }'
287
354
  },
288
355
 
289
356
  // ## Silent Mode
@@ -299,4 +366,4 @@ window.MercurySetup = {
299
366
  };
300
367
 
301
368
  if (!window.Mercury) window.Mercury = window.MercurySetup;
302
- else if (typeof(jQuery) !== 'undefined') jQuery.extend(window.Mercury, window.MercurySetup);
369
+ else if (typeof(jQuery) !== 'undefined') jQuery.extend(window.Mercury, window.MercurySetup);
@@ -0,0 +1,527 @@
1
+ /*
2
+ HTML Clean for jQuery
3
+ Anthony Johnston
4
+ http://www.antix.co.uk
5
+
6
+ version 1.2.3
7
+
8
+ $Revision: 51 $
9
+
10
+ requires jQuery http://jquery.com
11
+
12
+ Use and distibution http://www.opensource.org/licenses/bsd-license.php
13
+
14
+ 2010-04-02 allowedTags/removeTags added (white/black list) thanks to David Wartian (Dwartian)
15
+ 2010-06-30 replaceStyles added for replacement of bold, italic, super and sub styles on a tag
16
+ 2010-07-01 notRenderedTags added, where tags are to be removed but their contents are kept
17
+ */
18
+ (function ($) {
19
+ $.fn.htmlClean = function (options) {
20
+ // iterate and html clean each matched element
21
+ return this.each(function () {
22
+ if (this.value) {
23
+ this.value = $.htmlClean(this.value, options);
24
+ } else {
25
+ this.innerHTML = $.htmlClean(this.innerHTML, options);
26
+ }
27
+ });
28
+ };
29
+
30
+ // clean the passed html
31
+ $.htmlClean = function (html, options) {
32
+ options = $.extend({}, $.htmlClean.defaults, options);
33
+
34
+ var tagsRE = /<(\/)?(\w+:)?([\w]+)([^>]*)>/gi;
35
+ var attrsRE = /(\w+)=(".*?"|'.*?'|[^\s>]*)/gi;
36
+
37
+ var tagMatch;
38
+ var root = new Element();
39
+ var stack = [root];
40
+ var container = root;
41
+
42
+ if (options.bodyOnly) {
43
+ // check for body tag
44
+ if (tagMatch = /<body[^>]*>((\n|.)*)<\/body>/i.exec(html)) {
45
+ html = tagMatch[1];
46
+ }
47
+ }
48
+ html = html.concat("<xxx>"); // ensure last element/text is found
49
+ var lastIndex;
50
+
51
+ while (tagMatch = tagsRE.exec(html)) {
52
+ var tag = new Tag(tagMatch[3], tagMatch[1], tagMatch[4], options);
53
+
54
+ // add the text
55
+ var text = html.substring(lastIndex, tagMatch.index);
56
+ if (text.length > 0) {
57
+ var child = container.children[container.children.length - 1];
58
+ if (container.children.length > 0 && isText(child = container.children[container.children.length - 1])) {
59
+ // merge text
60
+ container.children[container.children.length - 1] = child.concat(text);
61
+ } else {
62
+ container.children.push(text);
63
+ }
64
+ }
65
+ lastIndex = tagsRE.lastIndex;
66
+
67
+ if (tag.isClosing) {
68
+ // find matching container
69
+ if (pop(stack, [tag.name])) {
70
+ stack.pop();
71
+ container = stack[stack.length - 1];
72
+ }
73
+ } else {
74
+ // create a new element
75
+ var element = new Element(tag);
76
+
77
+ // add attributes
78
+ var attrMatch;
79
+ while (attrMatch = attrsRE.exec(tag.rawAttributes)) {
80
+ // check style attribute and do replacements
81
+ if (attrMatch[1].toLowerCase() == "style" && options.replaceStyles) {
82
+
83
+ var renderParent = !tag.isInline;
84
+ for (var i = 0; i < options.replaceStyles.length; i++) {
85
+ if (options.replaceStyles[i][0].test(attrMatch[2])) {
86
+
87
+ if (!renderParent) {
88
+ tag.render = false;
89
+ renderParent = true;
90
+ }
91
+ container.children.push(element); // assumes not replaced
92
+ stack.push(element);
93
+ container = element; // assumes replacement is a container
94
+ // create new tag and element
95
+ tag = new Tag(options.replaceStyles[i][1], "", "", options);
96
+ element = new Element(tag);
97
+ }
98
+ }
99
+ }
100
+
101
+ if (tag.allowedAttributes != null
102
+ && (tag.allowedAttributes.length == 0
103
+ || $.inArray(attrMatch[1], tag.allowedAttributes) > -1)) {
104
+ element.attributes.push(new Attribute(attrMatch[1], attrMatch[2]));
105
+ }
106
+ }
107
+
108
+ // add required empty ones
109
+ $.each(tag.requiredAttributes, function () {
110
+ var name = this.toString();
111
+ if (!element.hasAttribute(name)) element.attributes.push(new Attribute(name, ""));
112
+ });
113
+
114
+ // check for replacements
115
+ for (var repIndex = 0; repIndex < options.replace.length; repIndex++) {
116
+ for (var tagIndex = 0; tagIndex < options.replace[repIndex][0].length; tagIndex++) {
117
+ var byName = typeof (options.replace[repIndex][0][tagIndex]) == "string";
118
+ if ((byName && options.replace[repIndex][0][tagIndex] == tag.name)
119
+ || (!byName && options.replace[repIndex][0][tagIndex].test(tagMatch))) {
120
+ // don't render this tag
121
+ tag.render = false;
122
+ container.children.push(element);
123
+ stack.push(element);
124
+ container = element;
125
+
126
+ // render new tag, keep attributes
127
+ tag = new Tag(options.replace[repIndex][1], tagMatch[1], tagMatch[4], options);
128
+ element = new Element(tag);
129
+ element.attributes = container.attributes;
130
+
131
+ repIndex = options.replace.length; // break out of both loops
132
+ break;
133
+ }
134
+ }
135
+ }
136
+
137
+ // check container rules
138
+ var add = true;
139
+ if (!container.isRoot) {
140
+ if (container.tag.isInline && !tag.isInline) {
141
+ add = false;
142
+ } else if (container.tag.disallowNest && tag.disallowNest
143
+ && !tag.requiredParent) {
144
+ add = false;
145
+ } else if (tag.requiredParent) {
146
+ if (add = pop(stack, tag.requiredParent)) {
147
+ container = stack[stack.length - 1];
148
+ }
149
+ }
150
+ }
151
+
152
+ if (add) {
153
+ container.children.push(element);
154
+
155
+ if (tag.toProtect) {
156
+ // skip to closing tag
157
+ var tagMatch2 = null;
158
+ while (tagMatch2 = tagsRE.exec(html)) {
159
+ var tag2 = new Tag(tagMatch2[3], tagMatch2[1], tagMatch2[4], options);
160
+ if (tag2.isClosing && tag2.name == tag.name) {
161
+ element.children.push(RegExp.leftContext.substring(lastIndex));
162
+ lastIndex = tagsRE.lastIndex;
163
+ break;
164
+ }
165
+ }
166
+ } else {
167
+ // set as current container element
168
+ if (!tag.isSelfClosing && !tag.isNonClosing) {
169
+ stack.push(element);
170
+ container = element;
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // render doc
178
+ return render(root, options).join("");
179
+ };
180
+
181
+ // defaults
182
+ $.htmlClean.defaults = {
183
+ // only clean the body tagbody
184
+ bodyOnly: true,
185
+ // only allow tags in this array, (white list), contents still rendered
186
+ allowedTags: [],
187
+ // remove tags in this array, (black list), contents still rendered
188
+ removeTags: ["basefont", "center", "dir", "font", "frame", "frameset", "iframe", "isindex", "menu", "noframes", "s", "strike", "u"],
189
+ // array of attribute names to remove on all elements in addition to those not in tagAttributes e.g ["width", "height"]
190
+ removeAttrs: [],
191
+ // array of [className], [optional array of allowed on elements] e.g. [["class"], ["anotherClass", ["p", "dl"]]]
192
+ allowedClasses: [],
193
+ // tags not rendered, contents remain
194
+ notRenderedTags: [],
195
+ // format the result
196
+ format: false,
197
+ // format indent to start on
198
+ formatIndent: 0,
199
+ // tags to replace, and what to replace with, tag name or regex to match the tag and attributes
200
+ replace: [
201
+ [
202
+ ["b", "big"],
203
+ "strong"
204
+ ],
205
+ [
206
+ ["i"],
207
+ "em"
208
+ ]
209
+ ],
210
+ // styles to replace with tags, multiple style matches supported, inline tags are replaced by the first match blocks are retained
211
+ replaceStyles: [
212
+ [/font-weight:\s*bold/i, "strong"],
213
+ [/font-style:\s*italic/i, "em"],
214
+ [/vertical-align:\s*super/i, "sup"],
215
+ [/vertical-align:\s*sub/i, "sub"]
216
+ ]
217
+ };
218
+
219
+ function applyFormat(element, options, output, indent) {
220
+ if (!element.tag.isInline && output.length > 0) {
221
+ output.push("\n");
222
+ for (var i = 0; i < indent; i++) output.push("\t");
223
+ }
224
+ }
225
+
226
+ function render(element, options) {
227
+ var output = [],
228
+ empty = element.attributes.length == 0,
229
+ indent = 0,
230
+ outputChildren = null;
231
+
232
+ // don't render if not in allowedTags or in removeTags
233
+ var renderTag
234
+ = element.tag.render
235
+ && (options.allowedTags.length == 0 || $.inArray(element.tag.name, options.allowedTags) > -1)
236
+ && (options.removeTags.length == 0 || $.inArray(element.tag.name, options.removeTags) == -1);
237
+
238
+ if (!element.isRoot && renderTag) {
239
+ // render opening tag
240
+ output.push("<");
241
+ output.push(element.tag.name);
242
+ $.each(element.attributes, function () {
243
+ if ($.inArray(this.name, options.removeAttrs) == -1) {
244
+ var m = new RegExp(/^(['"]?)(.*?)['"]?$/).exec(this.value);
245
+ var value = m[2];
246
+ var valueQuote = m[1] || "'";
247
+
248
+ // check for classes allowed
249
+ if (this.name == "class") {
250
+ value =
251
+ $.grep(value.split(" "), function (c) {
252
+ return $.grep(options.allowedClasses,
253
+ function (a) {
254
+ return a[0] == c && (a.length == 1 || $.inArray(element.tag.name, a[1]) > -1);
255
+ }).length > 0;
256
+ })
257
+ .join(" ");
258
+ valueQuote = "'";
259
+ }
260
+
261
+ if (value != null && (value.length > 0 || $.inArray(this.name, element.tag.requiredAttributes) > -1)) {
262
+ output.push(" ");
263
+ output.push(this.name);
264
+ output.push("=");
265
+ output.push(valueQuote);
266
+ output.push(value);
267
+ output.push(valueQuote);
268
+ }
269
+ }
270
+ });
271
+ }
272
+
273
+ if (element.tag.isSelfClosing) {
274
+ // self closing
275
+ if (renderTag) output.push(" />");
276
+ empty = false;
277
+ } else if (element.tag.isNonClosing) {
278
+ empty = false;
279
+ } else {
280
+ if (!element.isRoot && renderTag) {
281
+ // close
282
+ output.push(">");
283
+ }
284
+
285
+ indent = options.formatIndent++;
286
+
287
+ // render children
288
+ if (element.tag.toProtect) {
289
+ outputChildren = $.htmlClean.trim(element.children.join("")).replace(/<br>/ig, "\n");
290
+ output.push(outputChildren);
291
+ empty = outputChildren.length == 0;
292
+ options.formatIndent--;
293
+ } else {
294
+ outputChildren = [];
295
+ for (var i = 0; i < element.children.length; i++) {
296
+ var child = element.children[i];
297
+ var text = $.htmlClean.trim(textClean(isText(child) ? child : child.childrenToString()));
298
+ if (isInline(child)) {
299
+ if (i > 0 && text.length > 0
300
+ && (startsWithWhitespace(child) || endsWithWhitespace(element.children[i - 1]))) {
301
+ outputChildren.push(" ");
302
+ }
303
+ }
304
+ if (isText(child)) {
305
+ if (text.length > 0) {
306
+ outputChildren.push(text);
307
+ }
308
+ } else {
309
+ // don't allow a break to be the last child
310
+ if (i != element.children.length - 1 || child.tag.name != "br") {
311
+ if (options.format) applyFormat(child, options, outputChildren, indent);
312
+ outputChildren = outputChildren.concat(render(child, options));
313
+ }
314
+ }
315
+ }
316
+ options.formatIndent--;
317
+
318
+ if (outputChildren.length > 0) {
319
+ if (options.format && outputChildren[0] != "\n") applyFormat(element, options, output, indent);
320
+ output = output.concat(outputChildren);
321
+ empty = false;
322
+ }
323
+ }
324
+
325
+ if (!element.isRoot && renderTag) {
326
+ // render the closing tag
327
+ if (options.format) applyFormat(element, options, output, indent - 1);
328
+ output.push("</");
329
+ output.push(element.tag.name);
330
+ output.push(">");
331
+ }
332
+ }
333
+
334
+ // check for empty tags
335
+ if (!element.tag.allowEmpty && empty) {
336
+ return [];
337
+ }
338
+
339
+ return output;
340
+ }
341
+
342
+ // find a matching tag, and pop to it, if not do nothing
343
+ function pop(stack, tagNameArray, index) {
344
+ index = index || 1;
345
+ if ($.inArray(stack[stack.length - index].tag.name, tagNameArray) > -1) {
346
+ return true;
347
+ } else if (stack.length - (index + 1) > 0
348
+ && pop(stack, tagNameArray, index + 1)) {
349
+ stack.pop();
350
+ return true;
351
+ }
352
+ return false;
353
+ }
354
+
355
+ // Element Object
356
+ function Element(tag) {
357
+ if (tag) {
358
+ this.tag = tag;
359
+ this.isRoot = false;
360
+ } else {
361
+ this.tag = new Tag("root");
362
+ this.isRoot = true;
363
+ }
364
+ this.attributes = [];
365
+ this.children = [];
366
+
367
+ this.hasAttribute = function (name) {
368
+ for (var i = 0; i < this.attributes.length; i++) {
369
+ if (this.attributes[i].name == name) return true;
370
+ }
371
+ return false;
372
+ };
373
+
374
+ this.childrenToString = function () {
375
+ return this.children.join("");
376
+ };
377
+
378
+ return this;
379
+ }
380
+
381
+ // Attribute Object
382
+ function Attribute(name, value) {
383
+ this.name = name;
384
+ this.value = value;
385
+
386
+ return this;
387
+ }
388
+
389
+ // Tag object
390
+ function Tag(name, close, rawAttributes, options) {
391
+ this.name = name.toLowerCase();
392
+
393
+ this.isSelfClosing = $.inArray(this.name, tagSelfClosing) > -1;
394
+ this.isNonClosing = $.inArray(this.name, tagNonClosing) > -1;
395
+ this.isClosing = (close != undefined && close.length > 0);
396
+
397
+ this.isInline = $.inArray(this.name, tagInline) > -1;
398
+ this.disallowNest = $.inArray(this.name, tagDisallowNest) > -1;
399
+ this.requiredParent = tagRequiredParent[$.inArray(this.name, tagRequiredParent) + 1];
400
+ this.allowEmpty = $.inArray(this.name, tagAllowEmpty) > -1;
401
+
402
+ this.toProtect = $.inArray(this.name, tagProtect) > -1;
403
+
404
+ this.rawAttributes = rawAttributes;
405
+ this.allowedAttributes = tagAttributes[$.inArray(this.name, tagAttributes) + 1];
406
+ this.requiredAttributes = tagAttributesRequired[$.inArray(this.name, tagAttributesRequired) + 1];
407
+
408
+ this.render = options && $.inArray(this.name, options.notRenderedTags) == -1;
409
+
410
+ return this;
411
+ }
412
+
413
+ function startsWithWhitespace(item) {
414
+ while (isElement(item) && item.children.length > 0) {
415
+ item = item.children[0]
416
+ }
417
+ return isText(item) && item.length > 0 && $.htmlClean.isWhitespace(item.charAt(0));
418
+ }
419
+
420
+ function endsWithWhitespace(item) {
421
+ while (isElement(item) && item.children.length > 0) {
422
+ item = item.children[item.children.length - 1]
423
+ }
424
+ return isText(item) && item.length > 0 && $.htmlClean.isWhitespace(item.charAt(item.length - 1));
425
+ }
426
+
427
+ function isText(item) {
428
+ return item.constructor == String;
429
+ }
430
+
431
+ function isInline(item) {
432
+ return isText(item) || item.tag.isInline;
433
+ }
434
+
435
+ function isElement(item) {
436
+ return item.constructor == Element;
437
+ }
438
+
439
+ function textClean(text) {
440
+ return text.replace(/&nbsp;|\n/g, " ").replace(/\s\s+/g, " ");
441
+ }
442
+
443
+ // trim off white space, doesn't use regex
444
+ $.htmlClean.trim = function (text) {
445
+ return $.htmlClean.trimStart($.htmlClean.trimEnd(text));
446
+ };
447
+ $.htmlClean.trimStart = function (text) {
448
+ return text.substring($.htmlClean.trimStartIndex(text));
449
+ };
450
+ $.htmlClean.trimStartIndex = function (text) {
451
+ for (var start = 0; start < text.length - 1 && $.htmlClean.isWhitespace(text.charAt(start)); start++);
452
+ return start;
453
+ };
454
+ $.htmlClean.trimEnd = function (text) {
455
+ return text.substring(0, $.htmlClean.trimEndIndex(text));
456
+ };
457
+ $.htmlClean.trimEndIndex = function (text) {
458
+ for (var end = text.length - 1; end >= 0 && $.htmlClean.isWhitespace(text.charAt(end)); end--);
459
+ return end + 1;
460
+ };
461
+ // checks a char is white space or not
462
+ $.htmlClean.isWhitespace = function (c) {
463
+ return $.inArray(c, whitespace) != -1;
464
+ };
465
+
466
+ // tags which are inline
467
+ var tagInline = [
468
+ "a", "abbr", "acronym", "address", "b", "big", "br", "button",
469
+ "caption", "cite", "code", "del", "em", "font",
470
+ "hr", "i", "input", "img", "ins", "label", "legend", "map", "q",
471
+ "samp", "select", "small", "span", "strong", "sub", "sup",
472
+ "tt", "var"];
473
+ var tagDisallowNest = ["h1", "h2", "h3", "h4", "h5", "h6", "p", "th", "td"];
474
+ var tagAllowEmpty = ["th", "td"];
475
+ var tagRequiredParent = [
476
+ null,
477
+ "li", ["ul", "ol"],
478
+ "dt", ["dl"],
479
+ "dd", ["dl"],
480
+ "td", ["tr"],
481
+ "th", ["tr"],
482
+ "tr", ["table", "thead", "tbody", "tfoot"],
483
+ "thead", ["table"],
484
+ "tbody", ["table"],
485
+ "tfoot", ["table"]
486
+ ];
487
+ var tagProtect = ["script", "style", "pre", "code"];
488
+ // tags which self close e.g. <br />
489
+ var tagSelfClosing = ["br", "hr", "img", "link", "meta"];
490
+ // tags which do not close
491
+ var tagNonClosing = ["!doctype", "?xml"];
492
+ // attributes allowed on tags
493
+ var tagAttributes = [
494
+ ["class"], // default, for all tags not mentioned
495
+ "?xml", [],
496
+ "!doctype", [],
497
+ "a", ["accesskey", "class", "href", "name", "title", "rel", "rev", "type", "tabindex"],
498
+ "abbr", ["class", "title"],
499
+ "acronym", ["class", "title"],
500
+ "blockquote", ["cite", "class"],
501
+ "button", ["class", "disabled", "name", "type", "value"],
502
+ "del", ["cite", "class", "datetime"],
503
+ "form", ["accept", "action", "class", "enctype", "method", "name"],
504
+ "input", ["accept", "accesskey", "alt", "checked", "class", "disabled", "ismap", "maxlength", "name", "size", "readonly", "src", "tabindex", "type", "usemap", "value"],
505
+ "img", ["alt", "class", "height", "src", "width"],
506
+ "ins", ["cite", "class", "datetime"],
507
+ "label", ["accesskey", "class", "for"],
508
+ "legend", ["accesskey", "class"],
509
+ "link", ["href", "rel", "type"],
510
+ "meta", ["content", "http-equiv", "name", "scheme"],
511
+ "map", ["name"],
512
+ "optgroup", ["class", "disabled", "label"],
513
+ "option", ["class", "disabled", "label", "selected", "value"],
514
+ "q", ["class", "cite"],
515
+ "td", ["colspan", "rowspan"],
516
+ "th", ["colspan", "rowspan"],
517
+ "script", ["src", "type"],
518
+ "select", ["class", "disabled", "multiple", "name", "size", "tabindex"],
519
+ "style", ["type"],
520
+ "table", ["class", "summary"],
521
+ "textarea", ["accesskey", "class", "cols", "disabled", "name", "readonly", "rows", "tabindex"]
522
+ ];
523
+ var tagAttributesRequired = [[], "img", ["alt"]];
524
+ // white space chars
525
+ var whitespace = [" ", " ", "\t", "\n", "\r", "\f"];
526
+
527
+ })(jQuery);