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.
- data/POST_INSTALL +15 -0
- data/README.md +27 -6
- data/VERSION +1 -1
- data/app/controllers/mercury_controller.rb +4 -4
- data/app/views/layouts/mercury.html.erb +14 -3
- data/app/views/mercury/panels/snippets.html +1 -1
- data/app/views/mercury/snippets/{example_options.html.erb → example/options.html.erb} +0 -0
- data/app/views/mercury/snippets/{example.html.erb → example/preview.html.erb} +0 -0
- data/config/routes.rb +2 -2
- data/features/loading/loading.feature +22 -0
- data/features/loading/navigating.feature +77 -0
- data/features/loading/user_interface.feature +67 -0
- data/features/regions/editable/advanced_editing.feature +0 -0
- data/features/regions/editable/basic_editing.feature +195 -0
- data/features/regions/editable/inserting_links.feature +98 -0
- data/features/regions/editable/inserting_media.feature +110 -0
- data/features/regions/editable/inserting_snippets.feature +103 -0
- data/features/regions/editable/inserting_special_characters.feature +24 -0
- data/features/regions/editable/inserting_tables.feature +109 -0
- data/features/regions/editable/pasting.feature +0 -0
- data/features/regions/editable/uploading_images.feature +0 -0
- data/features/regions/markupable/advanced_editing.feature +0 -0
- data/features/regions/markupable/basic_editing.feature +0 -0
- data/features/regions/markupable/inserting_links.feature +0 -0
- data/features/regions/markupable/inserting_media.feature +0 -0
- data/features/regions/markupable/inserting_snippets.feature +0 -0
- data/features/regions/markupable/inserting_special_characters.feature +0 -0
- data/features/regions/markupable/inserting_tables.feature +0 -0
- data/features/regions/markupable/uploading_images.feature +0 -0
- data/features/regions/snippetable/advanced_editing.feature +0 -0
- data/features/regions/snippetable/basic_editing.feature +0 -0
- data/features/regions/snippetable/inserting_snippets.feature +0 -0
- data/features/saving/saving.feature +33 -0
- data/features/step_definitions/debug_steps.rb +2 -2
- data/features/step_definitions/mercury_steps.rb +441 -0
- data/features/support/env.rb +3 -3
- data/features/support/mercury_contents.rb +25 -0
- data/features/support/mercury_selectors.rb +147 -0
- data/features/support/paths.rb +20 -18
- data/features/support/selectors.rb +5 -3
- data/lib/generators/mercury/install/install_generator.rb +14 -0
- data/mercury-rails.gemspec +50 -20
- data/spec/javascripts/mercury/lightview_spec.js.coffee +55 -27
- data/spec/javascripts/mercury/mercury_spec.js.coffee +3 -3
- data/spec/javascripts/mercury/modal_spec.js.coffee +2 -2
- data/spec/javascripts/mercury/native_extensions_spec.js.coffee +0 -24
- data/spec/javascripts/mercury/page_editor_spec.js.coffee +148 -67
- data/spec/javascripts/mercury/panel_spec.js.coffee +2 -2
- data/spec/javascripts/mercury/region_spec.js.coffee +10 -7
- data/spec/javascripts/mercury/regions/editable_spec.js.coffee +0 -20
- data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +2 -2
- data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +1 -1
- data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +1 -1
- data/spec/javascripts/templates/mercury/page_editor.html +3 -3
- data/vendor/assets/images/mercury/close.png +0 -0
- data/vendor/assets/javascripts/mercury.js +140 -73
- data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-1.6.js +0 -0
- data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-ui-1.8.13.custom.js +0 -0
- data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery.additions.js +0 -0
- data/vendor/assets/javascripts/mercury/dependencies/jquery.htmlClean.js +527 -0
- data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/liquidmetal.js +0 -0
- data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/showdown.js +0 -0
- data/vendor/assets/javascripts/mercury/lightview.js.coffee +5 -2
- data/vendor/assets/javascripts/mercury/mercury.js.coffee +9 -8
- data/vendor/assets/javascripts/mercury/modals/htmleditor.js.coffee +3 -1
- data/vendor/assets/javascripts/mercury/modals/inserttable.js.coffee +2 -2
- data/vendor/assets/javascripts/mercury/native_extensions.js.coffee +6 -17
- data/vendor/assets/javascripts/mercury/page_editor.js.coffee +29 -8
- data/vendor/assets/javascripts/mercury/plugins/save_as_xml/mercury/page_editor.js.coffee +27 -0
- data/vendor/assets/javascripts/mercury/plugins/save_as_xml/plugin.js +9 -0
- data/vendor/assets/javascripts/mercury/region.js.coffee +2 -2
- data/vendor/assets/javascripts/mercury/regions/editable.js.coffee +89 -93
- data/vendor/assets/javascripts/mercury/regions/markupable.js.coffee +1 -1
- data/vendor/assets/javascripts/mercury/support/history.js +1 -0
- data/vendor/assets/javascripts/mercury/uploader.js.coffee +0 -1
- data/vendor/assets/javascripts/mercury_loader.js +4 -4
- data/vendor/assets/stylesheets/mercury/lightview.css +8 -0
- data/vendor/assets/stylesheets/mercury/mercury.css +12 -0
- data/vendor/assets/stylesheets/mercury/modal.css +0 -12
- data/vendor/assets/stylesheets/mercury/toolbar.css +1 -0
- data/vendor/assets/stylesheets/mercury_overrides.css +17 -0
- metadata +73 -45
- data/app/views/mercury/lightviews/imageprocessor.html +0 -3
- data/app/views/mercury/modals/sanitizer.html +0 -9
- data/features/editing/basic.feature +0 -11
- data/vendor/assets/images/mercury/clippy.png +0 -0
@@ -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="
|
11
|
-
<div id="region2" class="
|
12
|
-
<div id="region3" class="
|
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>
|
Binary file
|
@@ -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
|
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
|
-
|
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
|
-
'.
|
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-
|
285
|
-
'.mercury-
|
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);
|
File without changes
|
File without changes
|
data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery.additions.js
RENAMED
File without changes
|
@@ -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(/ |\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);
|