mercury-rails 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);