]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - themes/default/Wikiwyg/Wikiwyg/Wikitext.js
added wysiwyg_editor-1.3a feature by Jean-Nicolas GEREONE <jean-nicolas.gereone@st...
[SourceForge/phpwiki.git] / themes / default / Wikiwyg / Wikiwyg / Wikitext.js
1 /*==============================================================================
2 This Wikiwyg mode supports a textarea editor with toolbar buttons.
3
4 COPYRIGHT:
5
6     Copyright (c) 2005 Socialtext Corporation 
7     655 High Street
8     Palo Alto, CA 94301 U.S.A.
9     All rights reserved.
10
11 Wikiwyg is free software. 
12
13 This library is free software; you can redistribute it and/or modify it
14 under the terms of the GNU Lesser General Public License as published by
15 the Free Software Foundation; either version 2.1 of the License, or (at
16 your option) any later version.
17
18 This library is distributed in the hope that it will be useful, but
19 WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
21 General Public License for more details.
22
23     http://www.gnu.org/copyleft/lesser.txt
24
25  =============================================================================*/
26
27 proto = new Subclass('Wikiwyg.Wikitext', 'Wikiwyg.Mode');
28 klass = Wikiwyg.Wikitext;
29
30 proto.classtype = 'wikitext';
31 proto.modeDescription = 'Wikitext';
32
33 proto.config = {
34     supportCamelCaseLinks: false,
35     javascriptLocation: null,
36     clearRegex: null,
37     editHeightMinimum: 10,
38     editHeightAdjustment: 1.3,
39     markupRules: {
40         link: ['bound_phrase', '[', ']'],
41         bold: ['bound_phrase', '*', '*'],
42         code: ['bound_phrase', '`', '`'],
43         italic: ['bound_phrase', '/', '/'],
44         underline: ['bound_phrase', '_', '_'],
45         strike: ['bound_phrase', '-', '-'],
46         p: ['start_lines', ''],
47         pre: ['start_lines', '    '],
48         h1: ['start_line', '= '],
49         h2: ['start_line', '== '],
50         h3: ['start_line', '=== '],
51         h4: ['start_line', '==== '],
52         h5: ['start_line', '===== '],
53         h6: ['start_line', '====== '],
54         ordered: ['start_lines', '#'],
55         unordered: ['start_lines', '*'],
56         indent: ['start_lines', '>'],
57         hr: ['line_alone', '----'],
58         table: ['line_alone', '| A | B | C |\n|   |   |   |\n|   |   |   |']
59     }
60 }
61
62 proto.initializeObject = function() { // See IE
63     this.initialize_object();
64 }
65
66 proto.initialize_object = function() {
67     this.div = document.createElement('div');
68     this.textarea = document.createElement('textarea');
69     this.textarea.setAttribute('id', 'wikiwyg_wikitext_textarea');
70     this.div.appendChild(this.textarea);
71     this.area = this.textarea;
72     this.clear_inner_text();
73 }
74
75 proto.clear_inner_text = function() {
76     var self = this;
77     this.area.onclick = function() {
78         var inner_text = self.area.value;
79         var clear = self.config.clearRegex;
80         if (clear && inner_text.match(clear))
81             self.area.value = '';
82     }
83 }
84
85 proto.enableThis = function() {
86     this.superfunc('enableThis').call(this);
87     this.textarea.style.width = '100%';
88     this.setHeightOfEditor();
89     this.enable_keybindings();
90 }
91
92 proto.setHeightOfEditor = function() {
93     var config = this.config;
94     var adjust = config.editHeightAdjustment;
95     var area   = this.textarea;
96     var text   = this.textarea.value;
97     var rows   = text.split(/\n/).length;
98
99     var height = parseInt(rows * adjust);
100     if (height < config.editHeightMinimum)
101         height = config.editHeightMinimum;
102
103     area.setAttribute('rows', height);
104 }
105
106 proto.toWikitext = function() {
107     return this.textarea.value;
108 }
109
110 proto.toHtml = function(func) {
111     var wikitext = this.textarea.value;
112     this.convertWikitextToHtml(wikitext, func);
113 }
114
115 proto.fromHtml = function(html) {
116     this.textarea.value = 'Loading...';
117     var textarea = this.textarea;
118     this.convertHtmlToWikitext(
119         html, 
120         function(value) { textarea.value = value }
121     );
122 }
123
124 proto.fromWikitext = function(html) {
125     this.textarea.value = html;
126 }
127
128
129 proto.convertWikitextToHtml = function(wikitext, func) {
130     alert('Wikitext changes cannot be converted to HTML\nWikiwyg.Wikitext.convertWikitextToHtml is not implemented here');
131     func(this.copyhtml);
132 }
133
134 proto.convertHtmlToWikitext = function(html, func) {
135     func(this.convert_html_to_wikitext(html));
136 }
137
138 proto.get_keybinding_area = function() {
139     return this.textarea;
140 }
141
142 /*==============================================================================
143 Code to markup wikitext
144  =============================================================================*/
145 Wikiwyg.Wikitext.phrase_end_re = /[\s\.\:\;\,\!\?\(\)]/;
146
147 proto.find_left = function(t, selection_start, matcher) {
148     var substring = t.substr(selection_start - 1, 1);
149     var nextstring = t.substr(selection_start - 2, 1);
150     if (selection_start == 0) 
151         return selection_start;
152     if (substring.match(matcher)) {
153         // special case for word.word
154         if ((substring != '.') || (nextstring.match(/\s/))) 
155             return selection_start;
156     }
157     return this.find_left(t, selection_start - 1, matcher);
158 }  
159
160 proto.find_right = function(t, selection_end, matcher) {
161     var substring = t.substr(selection_end, 1);
162     var nextstring = t.substr(selection_end + 1, 1);
163     if (selection_end >= t.length)
164         return selection_end;
165     if (substring.match(matcher)) {
166         // special case for word.word
167         if ((substring != '.') || (nextstring.match(/\s/)))
168             return selection_end;
169     }
170     return this.find_right(t, selection_end + 1, matcher);
171 }
172
173 proto.get_lines = function() {
174     t = this.area;
175     var selection_start = t.selectionStart;
176     var selection_end = t.selectionEnd;
177
178     if (selection_start == null || selection_end == null)
179         return false
180
181     var our_text = t.value.replace(/\r/g, '');
182     selection = our_text.substr(selection_start,
183         selection_end - selection_start);
184
185     selection_start = this.find_right(our_text, selection_start, /[^\r\n]/);
186     selection_end = this.find_left(our_text, selection_end, /[^\r\n]/);
187
188     this.selection_start = this.find_left(our_text, selection_start, /[\r\n]/);
189     this.selection_end = this.find_right(our_text, selection_end, /[\r\n]/);
190     t.setSelectionRange(selection_start, selection_end);
191     t.focus();
192
193     this.start = our_text.substr(0,this.selection_start);
194     this.sel = our_text.substr(this.selection_start, this.selection_end -
195         this.selection_start);
196     this.finish = our_text.substr(this.selection_end, our_text.length);
197
198     return true;
199 }
200
201 proto.alarm_on = function() {
202     var area = this.area;
203     var background = area.style.background;
204     area.style.background = '#f88';
205
206     function alarm_off() {
207         area.style.background = background;
208     }
209
210     window.setTimeout(alarm_off, 250);
211     area.focus()
212 }
213
214 proto.get_words = function() {
215     function is_insane(selection) {
216         return selection.match(/\r?\n(\r?\n|\*+ |\#+ |\=+ )/);
217     }   
218
219     t = this.area;
220     var selection_start = t.selectionStart;
221     var selection_end = t.selectionEnd;
222     if (selection_start == null || selection_end == null)
223         return false;
224         
225     var our_text = t.value.replace(/\r/g, '');
226     selection = our_text.substr(selection_start,
227         selection_end - selection_start);
228
229     selection_start = this.find_right(our_text, selection_start, /(\S|\r?\n)/);
230     if (selection_start > selection_end)
231         selection_start = selection_end;
232     selection_end = this.find_left(our_text, selection_end, /(\S|\r?\n)/);
233     if (selection_end < selection_start)
234         selection_end = selection_start;
235
236     if (is_insane(selection)) {
237         this.alarm_on();
238         return false;
239     }
240
241     this.selection_start =
242         this.find_left(our_text, selection_start, Wikiwyg.Wikitext.phrase_end_re);
243     this.selection_end =
244         this.find_right(our_text, selection_end, Wikiwyg.Wikitext.phrase_end_re);
245
246     t.setSelectionRange(this.selection_start, this.selection_end);
247     t.focus();
248
249     this.start = our_text.substr(0,this.selection_start);
250     this.sel = our_text.substr(this.selection_start, this.selection_end -
251         this.selection_start);
252     this.finish = our_text.substr(this.selection_end, our_text.length);
253
254     return true;
255 }
256
257 proto.markup_is_on = function(start, finish) {
258     return (this.sel.match(start) && this.sel.match(finish));
259 }
260
261 proto.clean_selection = function(start, finish) {
262     this.sel = this.sel.replace(start, '');
263     this.sel = this.sel.replace(finish, '');
264 }
265
266 proto.toggle_same_format = function(start, finish) {
267     start = this.clean_regexp(start);
268     finish = this.clean_regexp(finish);
269     var start_re = new RegExp('^' + start);
270     var finish_re = new RegExp(finish + '$');
271     if (this.markup_is_on(start_re, finish_re)) {
272         this.clean_selection(start_re, finish_re);
273         return true;
274     }
275     return false;
276 }
277
278 proto.clean_regexp = function(string) {
279     string = string.replace(/([\^\$\*\+\.\?\[\]\{\}])/g, '\\$1');
280     return string;
281 }
282
283 proto.set_text_and_selection = function(text, start, end) {
284     this.area.value = text;
285     this.area.setSelectionRange(start, end);
286 }
287
288 proto.add_markup_words = function(markup_start, markup_finish, example) {
289     if (this.toggle_same_format(markup_start, markup_finish)) {
290         this.selection_end = this.selection_end -
291             (markup_start.length + markup_finish.length);
292         markup_start = '';
293         markup_finish = '';
294     }
295     if (this.sel.length == 0) {
296         if (example)
297             this.sel = example;
298         var text = this.start + markup_start +
299             this.sel + markup_finish + this.finish;
300         var start = this.selection_start + markup_start.length;
301         var end = this.selection_end + markup_start.length + this.sel.length;
302         this.set_text_and_selection(text, start, end);
303     } else {
304         var text = this.start + markup_start + this.sel +
305             markup_finish + this.finish;
306         var start = this.selection_start;
307         var end = this.selection_end + markup_start.length +
308             markup_finish.length;
309         this.set_text_and_selection(text, start, end);
310     }
311     this.area.focus();
312 }
313
314 // XXX - A lot of this is hardcoded.
315 proto.add_markup_lines = function(markup_start) {
316     var already_set_re = new RegExp( '^' + this.clean_regexp(markup_start), 'gm');
317     var other_markup_re = /^(\^+|\=+|\*+|#+|>+|    )/gm;
318
319     var match;
320     // if paragraph, reduce everything.
321     if (! markup_start.length) {
322         this.sel = this.sel.replace(other_markup_re, '');
323         this.sel = this.sel.replace(/^\ +/gm, '');
324     }
325     // if pre and not all indented, indent
326     else if ((markup_start == '    ') && this.sel.match(/^\S/m))
327         this.sel = this.sel.replace(/^/gm, markup_start);
328     // if not requesting heading and already this style, kill this style
329     else if (
330         (! markup_start.match(/[\=\^]/)) &&
331         this.sel.match(already_set_re)
332     ) {
333         this.sel = this.sel.replace(already_set_re, '');
334         if (markup_start != '    ')
335             this.sel = this.sel.replace(/^ */gm, '');
336     }
337     // if some other style, switch to new style
338     else if (match = this.sel.match(other_markup_re))
339         // if pre, just indent
340         if (markup_start == '    ')
341             this.sel = this.sel.replace(/^/gm, markup_start);
342         // if heading, just change it
343         else if (markup_start.match(/[\=\^]/))
344             this.sel = this.sel.replace(other_markup_re, markup_start);
345         // else try to change based on level
346         else
347             this.sel = this.sel.replace(
348                 other_markup_re,
349                 function(match) {
350                     return markup_start.times(match.length);
351                 }
352             );
353     // if something selected, use this style
354     else if (this.sel.length > 0)
355         this.sel = this.sel.replace(/^(.*\S+)/gm, markup_start + ' $1');
356     // just add the markup
357     else
358         this.sel = markup_start + ' ';
359
360     var text = this.start + this.sel + this.finish;
361     var start = this.selection_start;
362     var end = this.selection_start + this.sel.length;
363     this.set_text_and_selection(text, start, end);
364     this.area.focus();
365 }
366
367 // XXX - A lot of this is hardcoded.
368 proto.bound_markup_lines = function(markup_array) {
369     var markup_start = markup_array[1];
370     var markup_finish = markup_array[2];
371     var already_start = new RegExp('^' + this.clean_regexp(markup_start), 'gm');
372     var already_finish = new RegExp(this.clean_regexp(markup_finish) + '$', 'gm');
373     var other_start = /^(\^+|\=+|\*+|#+|>+) */gm;
374     var other_finish = /( +(\^+|\=+))?$/gm;
375
376     var match;
377     if (this.sel.match(already_start)) {
378         this.sel = this.sel.replace(already_start, '');
379         this.sel = this.sel.replace(already_finish, '');
380     }
381     else if (match = this.sel.match(other_start)) {
382         this.sel = this.sel.replace(other_start, markup_start);
383         this.sel = this.sel.replace(other_finish, markup_finish);
384     }
385     // if something selected, use this style
386     else if (this.sel.length > 0) {
387         this.sel = this.sel.replace(
388             /^(.*\S+)/gm,
389             markup_start + '$1' + markup_finish
390         );
391     }
392     // just add the markup
393     else
394         this.sel = markup_start + markup_finish;
395
396     var text = this.start + this.sel + this.finish;
397     var start = this.selection_start;
398     var end = this.selection_start + this.sel.length;
399     this.set_text_and_selection(text, start, end);
400     this.area.focus();
401 }
402
403 proto.markup_bound_line = function(markup_array) {
404     var scroll_top = this.area.scrollTop;
405     if (this.get_lines())
406         this.bound_markup_lines(markup_array);
407     this.area.scrollTop = scroll_top;
408 }
409
410 proto.markup_start_line = function(markup_array) {
411     var markup_start = markup_array[1];
412     markup_start = markup_start.replace(/ +/, '');
413     var scroll_top = this.area.scrollTop;
414     if (this.get_lines())
415         this.add_markup_lines(markup_start);
416     this.area.scrollTop = scroll_top;
417 }
418
419 proto.markup_start_lines = function(markup_array) {
420     var markup_start = markup_array[1];
421     var scroll_top = this.area.scrollTop;
422     if (this.get_lines())
423         this.add_markup_lines(markup_start);
424     this.area.scrollTop = scroll_top;
425 }
426
427 proto.markup_bound_phrase = function(markup_array) {
428     var markup_start = markup_array[1];
429     var markup_finish = markup_array[2];
430     var scroll_top = this.area.scrollTop;
431     if (markup_finish == 'undefined')
432         markup_finish = markup_start;
433     if (this.get_words())
434         this.add_markup_words(markup_start, markup_finish, null);
435     this.area.scrollTop = scroll_top;
436 }
437
438 klass.make_do = function(style) {
439     return function() {
440         var markup = this.config.markupRules[style];
441         var handler = markup[0];
442         if (! this['markup_' + handler])
443             die('No handler for markup: "' + handler + '"');
444         this['markup_' + handler](markup);
445     }
446 }
447
448 proto.do_link = klass.make_do('link');
449 proto.do_bold = klass.make_do('bold');
450 proto.do_code = klass.make_do('code');
451 proto.do_italic = klass.make_do('italic');
452 proto.do_underline = klass.make_do('underline');
453 proto.do_strike = klass.make_do('strike');
454 proto.do_p = klass.make_do('p');
455 proto.do_pre = klass.make_do('pre');
456 proto.do_h1 = klass.make_do('h1');
457 proto.do_h2 = klass.make_do('h2');
458 proto.do_h3 = klass.make_do('h3');
459 proto.do_h4 = klass.make_do('h4');
460 proto.do_h5 = klass.make_do('h5');
461 proto.do_h6 = klass.make_do('h6');
462 proto.do_ordered = klass.make_do('ordered');
463 proto.do_unordered = klass.make_do('unordered');
464 proto.do_hr = klass.make_do('hr');
465 proto.do_table = klass.make_do('table');
466
467 proto.do_dent = function(method) {
468     var scroll_top = this.area.scrollTop;
469     if (! this.get_lines()) {
470         this.area.scrollTop = scroll_top;
471         return;
472     }
473
474     if (method(this)) {
475         var text = this.start + this.sel + this.finish;
476         var start = this.selection_start;
477         var end = this.selection_start + this.sel.length;
478         this.set_text_and_selection(text, start, end);
479     }
480     this.area.focus();
481 }
482
483 proto.do_indent = function() {
484     this.do_dent(
485         function(that) {
486             if (that.sel == '') return false;
487             that.sel = that.sel.replace(/^(([\*\-\#])+(?=\s))/gm, '$2$1');
488             that.sel = that.sel.replace(/^([\>\=])/gm, '$1$1');
489             that.sel = that.sel.replace(/^([^\>\*\-\#\=\r\n])/gm, '> $1');
490             that.sel = that.sel.replace(/^\={7,}/gm, '======');
491             return true;
492         }
493     )
494 }
495
496 proto.do_outdent = function() {
497     this.do_dent(
498         function(that) {
499             if (that.sel == '') return false;
500             that.sel = that.sel.replace(/^([\>\*\-\#\=] ?)/gm, '');
501             return true;
502         }
503     )
504 }
505
506 proto.markup_line_alone = function(markup_array) {
507     var t = this.area;
508     var scroll_top = t.scrollTop;
509     var selection_start = t.selectionStart;
510     var text = t.value;
511     this.selection_start = this.find_right(text, selection_start, /\r?\n/);
512     this.selection_end = this.selection_start;
513     t.setSelectionRange(this.selection_start, this.selection_start);
514     t.focus();
515
516     var markup = markup_array[1];
517     this.start = t.value.substr(0, this.selection_start);
518     this.finish = t.value.substr(this.selection_end, t.value.length);
519     var text = this.start + '\n' + markup + this.finish;
520     var start = this.selection_start + markup.length + 1;
521     var end = this.selection_end + markup.length + 1;
522     this.set_text_and_selection(text, start, end);
523     t.scrollTop = scroll_top;
524 }
525
526
527 /*==============================================================================
528 Code to convert from html to wikitext.
529  =============================================================================*/
530 proto.convert_html_to_wikitext = function(html) {
531     this.copyhtml = html;
532     var dom = document.createElement('div');
533     html = html.replace(/<!-=-/g, '<!--').
534                 replace(/-=->/g, '-->');
535     dom.innerHTML = html;
536     this.output = [];
537     this.list_type = [];
538     this.indent_level = 0;
539
540     this.walk(dom);
541
542     // add final whitespace
543     this.assert_new_line();
544
545     return this.join_output(this.output);
546 }
547
548 proto.appendOutput = function(string) {
549     this.output.push(string);
550 }
551
552 proto.join_output = function(output) {
553     var list = this.remove_stops(output);
554     list = this.cleanup_output(list);
555     return list.join('');
556 }
557
558 // This is a noop, but can be subclassed.
559 proto.cleanup_output = function(list) {
560     return list;
561 }
562
563 proto.remove_stops = function(list) {
564     var clean = [];
565     for (var i = 0 ; i < list.length ; i++) {
566         if (typeof(list[i]) != 'string') continue;
567         clean.push(list[i]);
568     }
569     return clean;
570 }
571
572 proto.walk = function(element) {
573     if (!element) return;
574     for (var part = element.firstChild; part; part = part.nextSibling) {
575         if (part.nodeType == 1) {
576             this.dispatch_formatter(part);
577         }
578         else if (part.nodeType == 3) {
579             if (part.nodeValue.match(/\S/)) {
580                 var string = part.nodeValue;
581                 if (! string.match(/^[\.\,\?\!\)]/)) {
582                     this.assert_space_or_newline();
583                     string = this.trim(string);
584                 }
585                 this.appendOutput(this.collapse(string));
586             }
587         }
588     }
589 }
590
591 proto.dispatch_formatter = function(element) {
592     var dispatch = 'format_' + element.nodeName.toLowerCase();
593     if (! this[dispatch])
594         dispatch = 'handle_undefined';
595     this[dispatch](element);
596 }
597
598 proto.skip = function() { }
599 proto.pass = function(element) {
600     this.walk(element);
601 }
602 proto.handle_undefined = function(element) {
603     this.appendOutput('<' + element.nodeName + '>');
604     this.walk(element);
605     this.appendOutput('</' + element.nodeName + '>');
606 }
607 proto.handle_undefined = proto.skip;
608
609 proto.format_abbr = proto.pass;
610 proto.format_acronym = proto.pass;
611 proto.format_address = proto.pass;
612 proto.format_applet = proto.skip;
613 proto.format_area = proto.skip;
614 proto.format_basefont = proto.skip;
615 proto.format_base = proto.skip;
616 proto.format_bgsound = proto.skip;
617 proto.format_big = proto.pass;
618 proto.format_blink = proto.pass;
619 proto.format_body = proto.pass;
620 proto.format_br = proto.skip;
621 proto.format_button = proto.skip;
622 proto.format_caption = proto.pass;
623 proto.format_center = proto.pass;
624 proto.format_cite = proto.pass;
625 proto.format_col = proto.pass;
626 proto.format_colgroup = proto.pass;
627 proto.format_dd = proto.pass;
628 proto.format_dfn = proto.pass;
629 proto.format_dl = proto.pass;
630 proto.format_dt = proto.pass;
631 proto.format_embed = proto.skip;
632 proto.format_field = proto.skip;
633 proto.format_fieldset = proto.skip;
634 proto.format_font = proto.pass;
635 proto.format_form = proto.skip;
636 proto.format_frame = proto.skip;
637 proto.format_frameset = proto.skip;
638 proto.format_head = proto.skip;
639 proto.format_html = proto.pass;
640 proto.format_iframe = proto.pass;
641 proto.format_input = proto.skip;
642 proto.format_ins = proto.pass;
643 proto.format_isindex = proto.skip;
644 proto.format_label = proto.skip;
645 proto.format_legend = proto.skip;
646 proto.format_link = proto.skip;
647 proto.format_map = proto.skip;
648 proto.format_marquee = proto.skip;
649 proto.format_meta = proto.skip;
650 proto.format_multicol = proto.pass;
651 proto.format_nobr = proto.skip;
652 proto.format_noembed = proto.skip;
653 proto.format_noframes = proto.skip;
654 proto.format_nolayer = proto.skip;
655 proto.format_noscript = proto.skip;
656 proto.format_nowrap = proto.skip;
657 proto.format_object = proto.skip;
658 proto.format_optgroup = proto.skip;
659 proto.format_option = proto.skip;
660 proto.format_param = proto.skip;
661 proto.format_select = proto.skip;
662 proto.format_small = proto.pass;
663 proto.format_spacer = proto.skip;
664 proto.format_style = proto.skip;
665 proto.format_sub = proto.pass;
666 proto.format_submit = proto.skip;
667 proto.format_sup = proto.pass;
668 proto.format_tbody = proto.pass;
669 proto.format_textarea = proto.skip;
670 proto.format_tfoot = proto.pass;
671 proto.format_thead = proto.pass;
672 proto.format_wiki = proto.pass;
673
674 proto.format_img = function(element) {
675     var uri = element.getAttribute('src');
676     if (uri) {
677         this.assert_space_or_newline();
678         this.appendOutput(uri);
679     }
680 }
681
682 // XXX This little dance relies on knowning lots of little details about where
683 // indentation fangs are added and deleted by the various insert/assert calls.
684 proto.format_blockquote = function(element) {
685     if (! this.indent_level) {
686         this.assert_new_line();
687         this.indent_level++;
688         this.insert_new_line();
689     }
690     else {
691         this.indent_level++;
692         this.assert_new_line();
693     }
694     
695     this.walk(element);
696
697     this.indent_level--;
698
699     if (! this.indent_level)
700         this.assert_blank_line();
701     else
702         this.assert_new_line();
703 }
704
705 proto.format_div = function(element) {
706     if (this.is_opaque(element)) {
707         this.handle_opaque_block(element);
708         return;
709     }
710     this.walk(element);
711 }
712
713 proto.format_span = function(element) {
714     if (this.is_opaque(element)) {
715         this.handle_opaque_phrase(element);
716         return;
717     }
718
719     var style = element.getAttribute('style');
720     if (!style) {
721         this.pass(element);
722         return;
723     }
724
725     this.assert_space_or_newline();
726     if (style.match(/\bbold\b/))
727         this.appendOutput(this.config.markupRules.bold[1]);
728     if (style.match(/\bitalic\b/))
729         this.appendOutput(this.config.markupRules.italic[1]);
730     if (style.match(/\bunderline\b/))
731         this.appendOutput(this.config.markupRules.underline[1]);
732     if (style.match(/\bline-through\b/))
733         this.appendOutput(this.config.markupRules.strike[1]);
734
735     this.no_following_whitespace();
736     this.walk(element);
737
738     if (style.match(/\bline-through\b/))
739         this.appendOutput(this.config.markupRules.strike[2]);
740     if (style.match(/\bunderline\b/))
741         this.appendOutput(this.config.markupRules.underline[2]);
742     if (style.match(/\bitalic\b/))
743         this.appendOutput(this.config.markupRules.italic[2]);
744     if (style.match(/\bbold\b/))
745         this.appendOutput(this.config.markupRules.bold[2]);
746 }
747
748 klass.make_format = function(style) {
749     return function(element) {
750         var markup = this.config.markupRules[style];
751         var handler = markup[0];
752         this['handle_' + handler](element, markup);
753     }
754 }
755
756 proto.format_b = klass.make_format('bold');
757 proto.format_strong = proto.format_b;
758 proto.format_code = klass.make_format('code');
759 proto.format_kbd = proto.format_code;
760 proto.format_samp = proto.format_code;
761 proto.format_tt = proto.format_code;
762 proto.format_var = proto.format_code;
763 proto.format_i = klass.make_format('italic');
764 proto.format_em = proto.format_i;
765 proto.format_u = klass.make_format('underline');
766 proto.format_strike = klass.make_format('strike');
767 proto.format_del = proto.format_strike;
768 proto.format_s = proto.format_strike;
769 proto.format_hr = klass.make_format('hr');
770 proto.format_h1 = klass.make_format('h1');
771 proto.format_h2 = klass.make_format('h2');
772 proto.format_h3 = klass.make_format('h3');
773 proto.format_h4 = klass.make_format('h4');
774 proto.format_h5 = klass.make_format('h5');
775 proto.format_h6 = klass.make_format('h6');
776 proto.format_pre = klass.make_format('pre');
777
778 proto.format_p = function(element) {
779     this.assert_blank_line();
780     this.walk(element);
781     this.assert_blank_line();
782 }
783
784 proto.format_a = function(element) {
785     var label = Wikiwyg.htmlUnescape(element.innerHTML);
786     label = label.replace(/<[^>]*?>/g, ' ');
787     label = label.replace(/\s+/g, ' ');
788     label = label.replace(/^\s+/, '');
789     label = label.replace(/\s+$/, '');
790     this.make_wikitext_link(label, element.getAttribute('href'), element);
791 }
792
793 proto.format_table = function(element) {
794     this.assert_blank_line();
795     this.walk(element);
796     this.assert_blank_line();
797 }
798
799 proto.format_tr = function(element) {
800     this.walk(element);
801     this.appendOutput('|');
802     this.insert_new_line();
803 }
804
805 proto.format_td = function(element) {
806     this.appendOutput('| ');
807     this.walk(element);
808     this.appendOutput(' ');
809 }
810 proto.format_th = proto.format_td;
811
812 proto.format_ol = function(element) {
813     if (!this.list_type.length)
814         this.assert_blank_line();
815     else
816         this.assert_new_line();
817
818     this.list_type.push('ordered');
819     this.walk(element);
820     this.list_type.pop();
821
822     if (!this.list_type.length)
823         this.assert_blank_line();
824 }
825
826 // XXX - Maybe a refactoring for the duplication above
827 proto.format_ul = function(element) {
828     if (!this.list_type.length)
829         this.assert_blank_line();
830     else
831         this.assert_new_line();
832
833     this.list_type.push('unordered');
834     this.walk(element);
835     this.list_type.pop();
836
837     if (!this.list_type.length)
838         this.assert_blank_line();
839 }
840
841 proto.format_li = function(element) {
842     var level = this.list_type.length;
843     if (!level) die("List error");
844     var type = this.list_type[level - 1];
845     var markup = this.config.markupRules[type];
846     this.appendOutput(markup[1].times(level) + ' ');
847
848     this.walk(element);
849
850     this.chomp();
851     this.insert_new_line();
852 }
853
854
855 proto.chomp = function() {
856     var string;
857     while (this.output.length) {
858         string = this.output.pop();
859         if (typeof(string) != 'string') {
860             this.appendOutput(string);
861             return;
862         }
863         if (! string.match(/^\n>+ $/) && string.match(/\S/))
864             break;
865     }
866     if (string) {
867         string = string.replace(/[\r\n\s]+$/, '');
868         this.appendOutput(string);
869     }
870 }
871
872 proto.collapse = function(string) {
873     return string.replace(/[ \r\n]+/g, ' ');
874 }
875
876 proto.trim = function(string) {
877     return string.replace(/^\s+/, '');
878 }
879
880 proto.insert_new_line = function() {
881     var fang = '';
882     if (this.indent_level > 0)
883         fang = '>'.times(this.indent_level) + ' ';
884     // XXX - ('\n' + fang) MUST be in the same element in this.output so that
885     // it can be properly matched by chomp above.
886     if (this.output.length)
887         this.appendOutput('\n' + fang);
888     else if (fang.length)
889         this.appendOutput(fang);
890 }
891
892 proto.assert_new_line = function() {
893     this.chomp();
894     this.insert_new_line();
895 }
896
897 proto.assert_blank_line = function() {
898     this.chomp();
899     this.insert_new_line();
900     this.insert_new_line();
901 }
902
903 proto.assert_space_or_newline = function() {
904     var string;
905     if (! this.output.length) return;
906
907     string = this.output[this.output.length - 1];
908     
909     if (! string.whitespace && ! string.match(/\s+$/))
910         this.appendOutput(' ');
911 }
912
913 proto.no_following_whitespace = function() {
914     this.appendOutput({whitespace: 'stop'});
915 }
916
917 proto.handle_bound_phrase = function(element, markup) {
918     this.assert_space_or_newline();
919     this.appendOutput(markup[1]);
920     this.no_following_whitespace();
921     this.walk(element);
922     // assume that walk leaves no trailing whitespace.
923     this.appendOutput(markup[2]);
924 }
925
926 // XXX - A very promising refactoring is that we don't need the trailing
927 // assert_blank_line in block formatters.
928 proto.handle_bound_line = function(element,markup) {
929     this.assert_blank_line();
930     this.appendOutput(markup[1]);
931     this.walk(element);
932     this.appendOutput(markup[2]);
933     this.assert_blank_line();
934 }
935
936 proto.handle_start_line = function (element, markup) {
937     this.assert_blank_line();
938     this.appendOutput(markup[1]);
939     this.walk(element);
940     this.assert_blank_line();
941 }
942
943 proto.handle_start_lines = function (element, markup) {
944     var text = element.firstChild.nodeValue;
945     if (!text) return;
946     this.assert_blank_line();
947     text = text.replace(/^/mg, markup[1]);
948     this.appendOutput(text);
949     this.assert_blank_line();
950 }
951
952 proto.handle_line_alone = function (element, markup) {
953     this.assert_blank_line();
954     this.appendOutput(markup[1]);
955     this.assert_blank_line();
956 }
957
958 proto.get_first_comment = function(element) {
959     var comment = element.firstChild;
960     if (comment && (comment.nodeType == 8))
961         return comment;
962     else
963         return null;
964 }
965
966 proto.is_opaque = function(element) {
967     var comment = this.get_first_comment(element);
968     if (!comment) return false;
969
970     var text = comment.data;
971     if (text.match(/^\s*wiki:/)) return true;
972     return false;
973 }
974
975 proto.handle_opaque_phrase = function(element) {
976     var comment = this.get_first_comment(element);
977     if (comment) {
978         var text = comment.data;
979         text = text.replace(/^ wiki:\s+/, '');
980         text = text.replace(/\s$/, '');
981         this.assert_space_or_newline();
982         this.appendOutput(text);
983         this.assert_space_or_newline();
984     }
985 }
986
987 proto.handle_opaque_block = function(element) {
988     var comment = this.get_first_comment(element);
989     if (!comment) return;
990
991     var text = comment.data;
992     text = text.replace(/^\s*wiki:\s+/, '');
993     this.appendOutput(text);
994 }
995
996 proto.make_wikitext_link = function(label, href, element) {
997     var before = this.config.markupRules.link[1];
998     var after  = this.config.markupRules.link[2];
999
1000     this.assert_space_or_newline();
1001     if (! href) {
1002         this.appendOutput(label);
1003     }
1004     else if (href == label) {
1005         this.appendOutput(href);
1006     }
1007     else if (this.href_is_wiki_link(href)) {
1008         if (this.camel_case_link(label))
1009             this.appendOutput(label);
1010         else
1011             this.appendOutput(before + label + after);
1012     }
1013     else {
1014         this.appendOutput(before + href + ' ' + label + after);
1015     }
1016 }
1017
1018 proto.camel_case_link = function(label) {
1019     if (! this.config.supportCamelCaseLinks)
1020         return false;
1021     return label.match(/[a-z][A-Z]/);
1022 }
1023
1024 proto.href_is_wiki_link = function(href) {
1025     if (! this.looks_like_a_url(href))
1026         return true;
1027     if (! href.match(/\?/))
1028         return false;
1029     var no_arg_input   = href.split('?')[0];
1030     var no_arg_current = location.href.split('?')[0];
1031     return no_arg_input == no_arg_current;
1032 }
1033
1034 proto.looks_like_a_url = function(string) {
1035     return string.match(/^(http|https|ftp|irc|mailto):/);
1036 }
1037
1038 /*==============================================================================
1039 Support for Internet Explorer in Wikiwyg.Wikitext
1040  =============================================================================*/
1041 if (Wikiwyg.is_ie) {
1042
1043 proto.setHeightOf = function() {
1044     // XXX hardcode this until we can keep window from jumping after button
1045     // events.
1046     this.textarea.style.height = '200px';
1047 }
1048
1049 proto.initializeObject = function() {
1050     this.initialize_object();
1051     this.area.addBehavior(this.config.javascriptLocation + "Selection.htc");
1052 }
1053
1054 } // end of global if