Anonymous avatar Anonymous committed 9ce9878

[svn r10922] fixes #8850, #8851, #8852: implemented a lot of new functionalities, so named this version `1.1dev`

Comments (0)

Files changed (7)

+= General Notes =
+
+The CKEdiotrPLugin is in a beta phase.
+
+It was tested with 
+ - CKEditor 3.6.2
+ - Firefox 7 + 8
+ - Internet Explorer 8 + 9
+ 
+
+= Known Bugs / Limitations =
+
+ - marker and text color is not working in headers (see 
+   http://groups.google.com/group/trac-dev/browse_thread/thread/a6d12d574c3544ca)
+ - when inserting an image, only image name as URL is not working 
+   (even if there exists an image at that specific ticket / wiki entry)
+ - when entering a link manually, it is printed with an exclemation mark;
+   for example entering http://google.de is saved as http:!//google.de[[BR]]
+ - copying lists from MS Word (tested with Word 2003) is not always working 
+   completely (in some browsers it the deep intention is lost)
+    
+
+= Improvements Suggestions =
+
+== technical improvements (suggestions) ==
+
+The following approach might be a nicer solution as parsing the fragment and make a lot of 
+recursive calls. This approach is also used by bbcode-plugin 
+(see /CKEditor/_source/plugins/bbcode/plugin.js, line 135 and 311).
+
+{{{
+var parser = new CKEDITOR.htmlParser();
+var parserText = '';
+
+parser.onText = function( text )
+{
+	parserText += escapeFormatChars( text );
+};
+
+wiki = parser.parse( html );
+}}}

trunk/ckintegration/__init__.py

 
 from genshi import HTML
 from genshi.filters import Transformer
+from genshi.core import Markup
 from trac.core import * 
 from trac.web.chrome import add_stylesheet, add_script, add_script_data, ITemplateProvider
 from trac.web.api import ITemplateStreamFilter, IRequestHandler
-from trac.config import Option, ChoiceOption
+from trac.config import Option, ChoiceOption, ListOption
 from trac.mimeview.api import Context
 from trac.resource import Resource
-from trac.wiki.formatter import format_to
+from trac.wiki.formatter import format_to, Formatter, _markup_to_unicode,\
+    WikiProcessor
+from trac.wiki.parser import WikiParser
+from StringIO import StringIO
+import os
 
 __all__ = ['CkIntegrationModule']
 
+# copied from format_to_html in formatter.py
+def format_to_cke_html(env, context, wikidom, accepted_code_processors, escape_newlines=None):
+    if not wikidom:
+        return Markup()
+    if escape_newlines is None:
+        escape_newlines = context.get_hint('preserve_newlines', False)
+    return CKEditorFormatter(env, context, wikidom, accepted_code_processors).generate(escape_newlines)
+
+
+class CKEditorFormatter(Formatter):
+    """Extends base wiki formatter by setting code processor's name 
+for code blocks. Thus CKEditor can save it, so it could be processed 
+later by format processor like Pygments (see TracSyntaxColoring). 
+"""
+    
+    data_code_style = None
+    
+    def __init__(self, env, context, wikidom, accepted_code_processors):
+        self.env = env
+        self.context = context
+        self.accepted_code_processors = accepted_code_processors
+        if isinstance(wikidom, basestring):
+            wikidom = WikiParser(env).parse(wikidom)
+        self.wikidom = wikidom
+        Formatter.__init__(self, env, context)
+
+    # copied from HtmlFormatter
+    def generate(self, escape_newlines=False):
+        """Generate HTML elements.
+
+        newlines in the wikidom will be preserved if `escape_newlines` is set.
+        """
+        # FIXME: compatibility code only for now
+        out = StringIO()
+        self.format(self.wikidom, out, escape_newlines)
+#        self.env.log.debug('generated html: %s' % out.getvalue())
+        return Markup(out.getvalue())
+    
+    def handle_code_block(self, line, startmatch=None):
+        """Overrides Formatter.handle_code_block, so it 
+adds an additional `pre`-tag with attribute `data-code-style`,  
+in which the code-format is saved.
+
+Furthermore the code block is converted into HTML, because otherwise CKEditor 
+ignores empty lines. In this method linebreaks `\n` are replaced by `<br/>`.
+"""
+        handle_code_style = False
+        if line.strip() == WikiParser.ENDBLOCK and self.code_processor: 
+            clean_processor_name = self.code_processor.name
+            self.env.log.debug('clean_processor_name: %s' %  clean_processor_name) 
+            
+            idx = clean_processor_name.find('; ')
+            if idx >= 0:
+                clean_processor_name = clean_processor_name[:idx]
+            
+            if clean_processor_name == 'default':
+                handle_code_style = True
+                self.data_code_style = ''
+            elif clean_processor_name not in ['diff', 'td']:
+                try:
+                    from pygments.lexers import get_lexer_for_mimetype
+                    
+                    lexer = get_lexer_for_mimetype(clean_processor_name)
+                    proc_aliases = lexer.aliases
+                    if proc_aliases and len(proc_aliases) > 0:
+                        clean_processor_name = proc_aliases[0]
+                    else:
+                        clean_processor_name = lexer.name
+                    
+                    if clean_processor_name in self.accepted_code_processors:
+                        self.data_code_style = ' data-code-style="%s"' % clean_processor_name
+                        handle_code_style = True
+                except Exception, e:
+                    self.env.log.warn( "Error when retrieving lexer by mimetype: %s" % e )
+                    self.data_code_style = ''
+                
+        if handle_code_style:
+            self.env.log.debug('processing self.data_code_style: %s' %  self.data_code_style) 
+            code_text = os.linesep.join(self.code_buf)
+            html_text = WikiProcessor(self, 'default').process(code_text)
+            html_text = _markup_to_unicode( html_text )
+            html_text = html_text.replace('\n', '<br/>')
+            
+            html = HTML( html_text )
+            html |= Transformer('//pre').unwrap()
+            buffer = StringIO()
+            html.render(out=buffer, encoding='utf-8')
+            
+            self.out.write( '<pre%s>' % self.data_code_style )
+            self.out.write( _markup_to_unicode( buffer.getvalue() ) )
+            self.out.write('</pre>')
+            
+            self.in_code_block = 0
+        else:
+            Formatter.handle_code_block(self, line, startmatch)
+        
+
 class CkIntegrationModule(Component):
     """CKEditor integration for Trac
     
     
     Adds a request handler for AJAX-based TracWiki->HTML rendering.
     
-    The plugin supports 3 modes of integration, determined by the `editor_type` option.
+    The plugin supports several modes of integration, determined by the 
+    `editor_type` option (see Configuration section).
     
     The CKEditor itself is not built into the plugin, in order to allow the administrator
     to choose the layout and configuration freely ('''note that CKEditor >= 3.6 is required''').
     '''Disclaimer:''' This plugin is under development, and the `full_integration` mode
     is known to be experimental (at best) - only a handful of elements are supported.
     Feel free to join the effort to enhance the `full_integration` at
-    http://trac-hacks.org/wiki/CkEditorPlugin."""
+    http://trac-hacks.org/wiki/CkEditorPlugin.
+    
+    Configuration (config name, description, default values):
+    [[TracIni(ckeditor)]]"""
     implements(ITemplateProvider, ITemplateStreamFilter, IRequestHandler)
     
     editor_type = ChoiceOption('ckeditor', 'editor_type',
-        ['html_wrapper', 'full_integration', 'none'],
-        """Type of integrated editor.
-        || `html_wrapper` || CKEditor with HTML output wrapped in html-processor ||
-        || `full_integration` || CKEditor with TracWiki output (experimental) ||
-        || `none` || No integration - plain old textarea ||""")
+        ['full_integration', 'only_ticket', 'only_wiki', 'html_wrapper', 'none'],
+        """Type of integrated editor. Possible types are: 
+`full_integration`: CKEditor with TracWiki output ('''experimental'''), 
+`only_ticket`: CKEditor with TracWiki output for ticket fields ('''experimental'''); ''leaves wiki editing as in Trac standard'', 
+`only_wiki`: CKEditor with TracWiki output for wiki pages ('''experimental'''); ''leaves ticket editing as in Trac standard'',
+`html_wrapper`: CKEditor with HTML output wrapped in html-processor,  
+`none`: No integration - ''leaves editing as in Trac standard''""")
     
     editor_source = Option('ckeditor', 'editor_source', 'site/js/ckeditor/ckeditor.js',
         """Path to CKEditor 3.6.x javascript source.
         A recommended setup involves installing CKEditor in the htdocs/js directory
         of the Trac environment, and setting this option to site/js/ckeditor/ckeditor.js.""")
     
+    code_styles = ListOption('ckeditor', 'code_styles', 'cpp, csharp, java, js, python, sql, default, xml',
+        doc="""List of code styles, which should be processed by CKEditor and 
+        displayed in CKEditor dialog 'insert code'.""")
+    
 #    editor_replace = Option('ckeditor', 'editor_replace', '',
 #        """Javascript, which should replace textareas.""")
     
     def get_templates_dirs(self):
         return []
 
+    def _check_editor_type(self, filename):
+        """Checks whether editor is enabled for this view (filename).
+        Returns `true` if it is enabled, otherwise `false`.
+"""
+        if not self.editor_type or 'none' == self.editor_type:
+            return False
+        elif 'only_ticket' == self.editor_type:
+            return lower(filename) == 'ticket.html'
+        elif 'only_wiki' == self.editor_type:
+            return lower(filename) == 'wiki_edit.html'
+        else:
+            return lower(filename) in self.template_fields
+    
+    def get_styles_list(self):
+        style_list = [ ]
+        if self.code_styles:
+            style_opt_list = self.code_styles
+            self.log.info('self.code_styles: %s' % style_opt_list)
+            for style in style_opt_list:
+                if style == 'default':
+                    style_list.append(['Text', ''])
+                    continue
+                
+                try:
+                    from pygments.lexers import get_lexer_by_name    
+                    lexer = get_lexer_by_name(style)
+                    style_list.append([lexer.name, style])
+                except Exception, e:
+                    self.log.warn( "Error when retrieving lexer by name: %s" % e )
+        return style_list
+                    
     # ITemplateStreamFilter
     def filter_stream(self, req, method, filename, stream, data):
         self.log.debug("ckintegration: template %s" % (filename))
         # Act only when enabled, and editor_source defined, and current template has wiki-textareas
-        if 'none' != self.editor_type and self.editor_source and lower(filename) in self.template_fields:
+        if self.editor_source and self._check_editor_type(filename):
             # Some javascript global variable to add to the response to assist to tracwiki plugin
+            
             add_script_data(req, {
                             'ck_editor_type': self.editor_type,
-                            'ck_render_url':  req.href.ck_wiki_render(),
+                            'ck_code_styles': self.get_styles_list(),
+                            'trac_base_url':  req.href.base,
                             'ck_tracwiki_path': req.href.chrome('ckintegration'),
                             'ck_resource_realm': 'wiki',
                             'ck_resource_id': '',
             # Load the needed scripts (CKEditor itself, and the tracwiki plugin
             add_script(req, self.editor_source)
             add_script(req, 'ckintegration/tracwiki.js')
+            add_script(req, 'ckintegration/pastecode.js')
             # Inject a script that adds the tracwiki plugin as an external plugin to CKEditor
             # @todo: Perform init with a dedicated loader script
             # @todo: Use the init to modify the CKEditor toolbar
-            ck_plugin_init = '<script type="text/javascript">CKEDITOR.plugins.addExternal("tracwiki", ck_tracwiki_path, "tracwiki.js");</script>'
+            ck_plugin_init = '<script type="text/javascript">CKEDITOR.plugins.addExternal("tracwiki", ck_tracwiki_path, "tracwiki.js");\n'
+            ck_plugin_init += 'CKEDITOR.plugins.addExternal("pastecode", ck_tracwiki_path, "pastecode.js");</script>'
             stream |= Transformer('.//body').prepend(HTML(ck_plugin_init))
             #add_script(req, 'ckintegration/ckloader.js')
             # Replace all relevant textarea fields in the template with CKEditor instances
             for field_name in self.template_fields[lower(filename)]:
-                self.log.info('Replacing textarea "%s" with CKEditor instance' % (field_name))
+                self.log.debug('Replacing textarea "%s" with CKEditor instance' % (field_name))
                 add_editor = '''<script type="text/javascript">
-                    CKEDITOR.replace("%s", { extraPlugins : "tracwiki" });
+                    CKEDITOR.replace("%s", { extraPlugins : "tracwiki,pastecode" });
                 </script>''' % (field_name)
                 #self.log.debug ("add_editor is %s" % add_editor)
                 stream |= Transformer('.//textarea[@name="%s"]' % (field_name)).after(HTML(add_editor))
                             not f.has_key('format') or not 'wiki' == lower(f['format']):
                         continue 
                     field_name = 'field_%s' % f['name']
-                    self.log.info('Replacing textarea "%s" with CKEditor instance' % (field_name))
+                    self.log.debug('Replacing textarea "%s" with CKEditor instance' % (field_name))
                     add_editor = '''<script type="text/javascript">
-                        CKEDITOR.replace("%s", { extraPlugins : "tracwiki" });
+                        CKEDITOR.replace("%s", { extraPlugins : "tracwiki,pastecode" });
                     </script>''' % (field_name)
                     stream |= Transformer('.//textarea[@name="%s"]' % (field_name)).after(HTML(add_editor))
         return stream
         
         resource = Resource(realm, id=id, version=version)
         context = Context.from_request(req, resource)
-        rendered = format_to(self.env, flavor, context, text, **options)
-        req.send(rendered.encode('utf-8'))
+        rendered = format_to_cke_html(self.env, context, text, self.code_styles, **options)
+        
+        # since Trac renders underlined text as `<span class="underlined">text</span>
+        # instead of u-tag, we need to adjust it for compatibility's sake
+        # see also discussion at Google Groups:
+        # https://groups.google.com/group/trac-dev/browse_thread/thread/833206a932d1f918
+        html = HTML(rendered)
+        html |= Transformer('//span[@class="underline"]').rename('u').attr('class', None)
+        # CKEditor renders indentation by using p style="margin-left: 40px" 
+        # instead of blockquote-tag
+        html |= Transformer('//blockquote/p').attr('style', 'margin-left: 40px')
+        html |= Transformer('//blockquote').unwrap()
+        buffer = StringIO()
+        html.render(out=buffer, encoding='utf-8')
+        req.send( buffer.getvalue() )
+        
Add a comment to this file

trunk/ckintegration/htdocs/images/pastecode.png

Added
New image

trunk/ckintegration/htdocs/pastecode.js

+CKEDITOR.plugins.add('pastecode', {
+	getBasePath : function( )
+	{
+		return ck_tracwiki_path ? ck_tracwiki_path : this.path;
+	},
+	init : function(editor) {
+		editor.addCommand('pcodeDialog', new CKEDITOR.dialogCommand('pcodeDialog'));
+		editor.ui.addButton('PasteCode', {
+			label : 'Quellcode einfügen', // TODO: translate (en: "Paste code")
+			icon : this.getBasePath() + '/images/pastecode.png',
+			command : 'pcodeDialog'
+		});
+
+		CKEDITOR.dialog.add('pcodeDialog', function(editor) {
+			return {
+				title : 'Quellcode einfügen', // TODO: translate (en: "Paste code")
+				minWidth : 400,
+				minHeight : 200,
+				contents : [ {
+					id : 'tab1',
+					label : 'First Tab',
+					title : 'First Tab',
+					elements : 
+					[ 
+					  {
+						id : 'code',
+						// FIXME: why is style not working? (doesn't display textarea in monospace)
+						style : 'font-family: monospace;',
+						type : 'textarea',
+						label : 'Code', // TODO: translate (en: "Code")
+						setup : function( element )
+						{
+							var text = ''; // default value
+							if (element)
+							{
+								if (element.getChildren())
+								{
+									var childs = element.getChildren();
+									for (var i = 0; i < childs.count(); i++)
+									{
+										if (childs.getItem(i).getText().length > 0)
+											text += childs.getItem(i).getText() + '\n';
+									}
+								} else
+									text = element.getText();
+							}
+							this.setValue( text );
+						},
+						commit : function( element )
+						{
+							if ( element && element.getName() == 'pre' )
+								element.appendText( this.getValue() );
+						}
+					  },
+					  {
+						id : 'pr_style',
+						type : 'select',
+						items: ck_code_styles,
+						label : 'Syntax Highlighting', // TODO: translate (en: "Style (Syntax highlighting)")
+						setup : function( element )
+						{
+						  	// default value; TODO: make it configurable
+						  	var prog_lang = 'default'; 
+						  	if ( element && element.data( 'code-style' ) )
+						  		prog_lang = element.data( 'code-style' );
+							this.setValue( prog_lang );
+						},
+						commit : function( element )
+						{
+							if ( element && element.getName() == 'pre' )
+							{
+								if ( this.getValue() )
+									element.data( 'code-style', this.getValue() );
+								else
+									element.data( 'code-style', false );
+							}
+						}
+					  } 
+					]
+				} ],
+				onShow : function()
+				{
+					var sel = editor.getSelection();
+					this.element = sel ? sel.getStartElement() : null;
+					this.editMode = false;
+					
+					if ( this.element )
+					{
+						this.element = this.element.getAscendant( 'pre', true );
+						if ( this.element )
+						{
+							var data_el = this.element.getAscendant( 'pre', false );
+							if ( data_el && data_el.data('code-style') )
+								this.element = data_el;
+						}
+					}
+					
+					if (!this.element || this.element.getName() != 'pre' )
+						this.element = CKEDITOR.dom.element.createFromHtml( '<pre></pre>' );
+					else
+						this.editMode = true;
+					
+					this.setupContent( this.element );
+					var field = this.getContentElement( 'tab1', 'code' );
+					field.focus();
+				},
+				onOk: function () {
+					if ( this.editMode )
+					{
+						this.element.setHtml( '' );
+						this.commitContent( this.element );
+					} else
+					{
+						this.element = CKEDITOR.dom.element.createFromHtml( '<pre></pre>' );
+						this.commitContent( this.element );
+						editor.insertElement( this.element );
+					}
+				}
+			};
+		});
+	}
+});

trunk/ckintegration/htdocs/tracwiki.js

 
 (function()
 {
+	// CONSTANTS
+	/** special wiki format characters */
+	var ESCAPE_WIKI_MARKUP = [ ["''", "!''"], // implies ["'''", "!'''"], 
+	                   ['\\/\\/', '!//'], // alternative italic
+	                   ['\\*\\*', '!**'], // alternative bold
+	                   ['\\\\\\\\', '!\\\\'], // alternative BR (double backslashes)
+	                   ['__', '!__'], 
+	                   ['`', '!`'], 
+	                   ['~~', '!~~'], 
+	                   ['\\^', '!^'], 
+	                   [',,', '!,,'], 
+	                   ['!$', '! '], 
+	                   ['\\|\\|', '!||'],
+	                   ['\\[\\[', '![['] // macros
+	                 ];
+	
+	/** format of text:
+	 * null (default) HTML, keep formats
+	 * 0 pasteFromWord, keep formats
+	 * 1 pasteText, paste only text
+	 */
+	var defaultPasteFormat = null;
+	
+	function readConfig( config )
+	{
+		if (config)
+		{
+			if ( config.defaultPasteFormat == undefined || config.defaultPasteFormat == null )
+				defaultPasteFormat = null;
+			else if ( config.defaultPasteFormat == 0 || config.defaultPasteFormat == 1)
+				defaultPasteFormat = config.defaultPasteFormat;
+		}
+	}
+	
+	function cleanHtmlEntities ( value )
+	{
+		value = value.replace(/&nbsp;/g, ' ');
+		value = value.replace(/&quot;/g, '\"');
+		value = value.replace(/&amp;/g, '\&');
+		value = value.replace(/&lt;/g, '\<');
+		value = value.replace(/&gt;/g, '\>');
+		return value;
+	}
+	
+	function escapeFormatChars( value )
+	{
+		for ( var i = 0; i < ESCAPE_WIKI_MARKUP.length; i++ )
+		{
+			var regex = new RegExp(ESCAPE_WIKI_MARKUP[i][0], "g");
+			value = value.replace(regex, ESCAPE_WIKI_MARKUP[i][1]);
+		}
+		
+		return value;
+	}
+	
+	function getStyleAttribute( attr, regex, value )
+	{
+		var re = new RegExp("^" + attr + ":\s*(" + regex + ")", "i");
+		var result = re.exec(value);
+		if ( result != null )
+		{
+			return result[1];
+		}
+		return null;
+	}
+	
 	CKEDITOR.plugins.add( 'tracwiki',
 	{
 		requires : [ 'htmlwriter' ],
 		init : function( editor )
 		{
 			var dataProcessor = editor.dataProcessor = new CKEDITOR.tracwikiDataProcessor( editor );
-
+			
 			dataProcessor.writer.forceSimpleAmpersand = editor.config.forceSimpleAmpersand;
+			readConfig( editor.config );
 		},
 
 		onLoad : function()
 			! ( 'fillEmptyBlocks' in CKEDITOR.config ) && ( CKEDITOR.config.fillEmptyBlocks = 1 );
 		}
 	});
+	
+	//When Pasting Text the Text is converted to wikiCode
+	CKEDITOR.on('instanceReady', function ( editor ) {
+		editor.editor.on('afterCommandExec', function ( event ) {
+			var commandName = event.data.name;
+			if ( !CKEDITOR.currentInstance )
+				return;
+			var dataProcessor = CKEDITOR.currentInstance.dataProcessor;
+			
+			if ( commandName == 'pastefromword' )
+				dataProcessor.format = 0;
+			else if ( commandName.substring(0,5) == 'paste')
+				dataProcessor.format = 1;
+			else
+				dataProcessor.format = null;
+		});
+		
+		editor.editor.on('paste', function ( editor ) {
+			var dataProcessor = CKEDITOR.currentInstance ? CKEDITOR.currentInstance.dataProcessor : this.editor.dataProcessor;
+			if ( editor.data.html )
+			{
+				if ( dataProcessor.format == null )				
+					dataProcessor.format = defaultPasteFormat;
+				editor.data.html = dataProcessor.toDataFormat(editor.data.html, "p");
+			} else
+			{
+				dataProcessor.format = 1;
+				editor.data.html = dataProcessor.toDataFormat(editor.data.text, "pre");
+			}
+		});
+	});
 
 	CKEDITOR.tracwikiDataProcessor = function( editor )
 	{
 			ajax_data["__FORM_TOKEN"] = form_token;
 			ajax_data["text"] = data;
 			var ajaxResponse = jQuery.ajax({
-				type: "POST", url: ck_render_url,
+				type: "POST", url: trac_base_url + '/ck_wiki_render',
 				data: ajax_data, dataType: "html", async: false
 				//success: this.ajaxSuccess, error: this.ajaxError
 				});
 		
 		toDataFormat : function( html, fixForBody )
 		{
-			if ( 'html_wrapper' == ck_editor_type )
-			{
+			var list_str, offset = '', tablewidth, tableheight;
+			var hpattern = /^h\d/i;
+			var intablepattern = /t([d|h|r|body|head|able])/i;
+			var inlistpattern = /([ol|ul])/i;
+			var widthpattern = /width:\s(\d+)(px|%)/i;
+			var heightpattern = /height:\s(\d+)(px|%)/i;
+			var marginpattern = /margin-left:\s(\d+)px/i;
+			var urlpattern = /(http:\/\/)?(.*\.[gif|jpg|png|bmp])/i;
+			var programmingStyle = null;
+			var onlyText = null;
+			var code_lang = '';
+
+			if ( 'html_wrapper' == ck_editor_type ) {
 				return '{{{\n#!html\n' + html + '\n}}}';
 			}
 			
 			var writer = this.writer;
 			
+			this.trim = function( line ){
+				line = line.replace(/^\s*/,'');
+				line = line.replace(/\s*$/,'');
+				return line;
+			}
+			
+			this.getHref = function ( href )
+			{
+				if (href == undefined || href == null)
+					return href;
+				var re = new RegExp('^(' + trac_base_url + ')+(.*)');
+				var result = re.exec(href);
+				if (result && result.length > 2 && result[1] != null && result[2] != null)
+					href = result[2];
+				return href;
+			}
+			
 			this.getHtmlBlock = function( fragment )
 			{
 				writer.reset();
 				fragment.writeHtml( writer, this.htmlFilter );
-				return '{{{\n#!html\n' + writer.getHtml(true) + '\n}}}\n';
+				return '[[html(' + writer.getHtml(true) + ')]]';
 			};
 			
-			var list_str = '1. ';
-			
+
 			// @todo: Handle HTML->TracWiki conversion in an extensible way...
 			this.parseFragment = function( fragment )
 			{
 				var data = '',
-				    frag;
+				 	frag,
+					style;
 				for (var i = 0; i < fragment.children.length; i++)
 				{
 					frag = fragment.children[i];
+					
+					if (this.programmingStyle == true && frag.name != 'pre')
+					{
+						if (frag.value)
+							data += cleanHtmlEntities( frag.value );
+						else if (frag.children)
+							data += this.parseText(frag);
+						continue;
+					}
+					
 					if (frag.name)
 					{
+						var subcontent;
+						var linebreaks;
+						if (frag.attributes && frag.attributes.style
+							&& (frag.name == 'span' || frag.name == 'div')
+							)
+						{
+							subcontent = this.parseFragment(frag);
+							var stylesArray = frag.attributes.style.split(';');
+							// other styles than background-color or color should be ignored
+							var allowedStyles = [ 'background-color', 'color' ];
+							var style = '';
+							
+							for (var i2 = 0; i2 < stylesArray.length; i2++)
+							{
+								if (stylesArray[i2] == '')
+									continue;
+								for (i3 = 0; i3 < allowedStyles.length; i3++)
+								{
+									var s = getStyleAttribute(allowedStyles[i3], '.*', stylesArray[i2]);
+									if (s)
+										style += allowedStyles[i3] + ':' + s;
+								}
+							}
+							
+							var ending = "";
+							if (subcontent.charAt(subcontent.length - 1) == ' ')
+							{ 
+								// make sure ending space is outside of macro
+								// otherwise it will be omitted in Trac
+								subcontent = subcontent.substring(0, subcontent.length - 1)
+								ending = " ";
+							}
+
+							var macro_call = "";
+							if (frag.name == 'span')
+							{
+								macro_call = '[[span(' + subcontent + ', style=' + style + ')]]'
+							} else if (frag.name == 'div')
+							{
+								macro_call = '{{{#!div style="' + style + '"\n' + subcontent + '\n}}}'
+							}	
+							data += macro_call + ending;
+							continue;
+						} 
+						
+						switch (frag.name) {
+						// Headlines
+						case 'h1':
+							data += '\n' + '= ' + this.parseFragment(frag) + ' = \n';
+							break;
+						case 'h2':
+							data += '\n' + '== ' + this.parseFragment(frag) + ' == \n';
+							break;
+						case 'h3':
+							data += '\n' + '=== ' + this.parseFragment(frag) + ' === \n';
+							break;
+						case 'h4':
+							data += '\n' + '==== ' + this.parseFragment(frag) + ' ==== \n';
+							break;
+						case 'h5':
+							data += '\n' + '===== ' + this.parseFragment(frag) + ' ===== \n';
+							break;
+						case 'h6':
+							data += '\n' + '====== ' + this.parseFragment(frag) + ' ====== \n';
+							break;
+
+						// Textformat
+						case 'u':
+							data += '__' + this.parseFragment(frag) + '__';
+							break;
+						
+						case 'b': //fall through
+						case 'strong':
+							data += "'''" + this.parseFragment(frag) + "'''";
+							break;
+						
+						case 'i': // fall through
+						case 'em':
+							data += "''" + this.parseFragment(frag) + "''";
+							break;
+						case 'sup':
+							data += '^' + this.parseFragment(frag) + '^';
+							break;
+						case 'sub':
+							data += ',,' + this.parseFragment(frag) + ',,';
+							break;
+						case 'strike':
+							data += '~~' + this.parseFragment(frag) + '~~';
+							break;
+						case 'del':
+							data += '~~' + this.parseFragment(frag) + '~~';
+							break;
+						case 'tt':
+							data += '`' + this.parseFragment(frag) + '`';
+							break;
+						case 'hr': // horizontal row
+							data += '\n----\n';
+							break;
+						case 'a': // hyperlinks
+							this.onlyText = true; // don't escape double slashes or other Wiki-Formatting
+							subcontent = this.parseFragment(frag);
+							this.onlyText = false;
+							if (frag.attributes.rel && frag.attributes.rel.match('nofollow') != null)
+							{
+								subcontent = subcontent.replace("\?", "");
+								data += subcontent;
+							} else if (frag.attributes['class'] && frag.attributes['class'].match('wiki') != null)
+							{
+								data += subcontent;
+							} else if ( subcontent.match(/\s*\[\[/) // image
+									|| subcontent.match(/\s*#\d+/) // ticket number
+									|| subcontent.match(/\s*\[\d+\]/) // revision number
+									|| subcontent.match(/\s*\{\d+\}/) // report number
+								)
+							{
+								data += subcontent; // it is an image
+							} else if (frag.attributes.href)
+							{
+								var href = this.getHref( frag.attributes.href );
+								if (subcontent.length > 0)
+									data += "[" + href + " " + this.trim(subcontent) + "]";
+								else
+									data += href;
+							} else
+							{
+								data += subcontent;
+							}
+							break;
+						case 'img': // hyperlinks
+							if (frag.attributes.src && frag.attributes.src.match(urlpattern))
+							{
+								var src = this.getHref( frag.attributes.src );
+								data += '[[Image(' + src + ')]]';
+							} else
+								data += this.getHtmlBlock(frag);
+							break;
+
+						// Container
+						case 'p':
+							linebreaks = true;
+							var offsetold = offset;
+							offset += this.getMarginLeftOffset(frag);
+							subcontent = this.parseFragment(frag);
+							var par = frag.parent;
+							while (par && par.name) {
+								if (par.name.match("td|th|li") != null) {
+									linebreaks = false;
+									break;
+								}
+								par = parent.parent;
+							}
+							if (subcontent && (this.trim(subcontent)).length > 0) {
+								if (linebreaks && !subcontent.match(/\n+$/i))
+								{
+									data += offset + subcontent + "\n\n";
+								} 
+								else
+								{
+									data += offset + subcontent;
+								}
+							}
+							offset = offsetold;
+							break;
+						case 'blockquote':
+							offset += ' ';
+							data += this.parseFragment(frag);
+							linebreaks =!data.match(/\n\n$/);
+							if (linebreaks)
+								data +='\n\n';
+							offset = offset.replace(/^\s/, '');
+							break;
+						// when pasting from MS Word or OpenOffice, many font-nodes are present 
+						case 'font':
+							// fall through
+						case 'div':
+							data += this.parseFragment(frag);
+							break;
+						case 'pre':
+							this.programmingStyle = true;
+							var c = this.parseFragment(frag);
+							c = c.replace(/\n*$/, "");
+							
+							if ( frag.attributes['data-code-style'] )
+								this.code_lang = '#!' + frag.attributes['data-code-style'];
+
+							if (c && c.length > 0)
+							{
+								data += '\n{{{';
+								if ( this.code_lang )
+									data += this.code_lang;
+								data += '\n' + c + '\n}}}\n';
+								frag.children = [];
+							}
+							this.code_lang = '';
+							this.programmingStyle = false;
+							break;
+						case 'span':
+							data += this.parseFragment(frag);
+							break;
+
+						case 'ul': // unordered List
+							list_str = '- ';
+							offset += ' ';
+							data += this.parseFragment(frag)
+							if (!frag.parent || !frag.parent.name || !frag.parent.name.match(inlistpattern))
+								data += '\n\n';
+							offset = offset.replace(/^\s/, '');
+							break;
+						case 'ol': // ordered list
+							if (frag.attributes.start)
+								list_str = frag.attributes.start + '. ';
+							else
+								list_str = '1. ';
+							offset += ' ';
+							data += this.parseFragment(frag)
+							if (!frag.parent || !frag.parent.name || !frag.parent.name.match(inlistpattern))
+								data += '\n';
+							offset = offset.replace(/^\s/, '');
+							break;
+						case 'li':
+							var subcontent = this.parseFragment(frag);
+							data += '\n' + offset + list_str + subcontent;
+							break;
+
+						case 'br': // line break
+							if (frag.parent && frag.parent.name) {
+								var parentName = frag.parent.name;
+								if (parentName.match(hpattern))
+								{
+									// ignore br-tags in headers
+									break;
+								} else if (!parentName.match(intablepattern) && 
+										parentName != 'li') {
+									data += '[[BR]]\n';
+									break;
+								} else
+								{
+									data += '[[BR]]';
+								}
+							}							
+							break;
+
+						case 'table': // table
+							offset += ' ';
+							if (frag.attributes.style && frag.attributes.style != null) {
+								tablewidth = frag.attributes.style.match(widthpattern);
+								tableheight = frag.attributes.style.match(heightpattern);
+							}
+							data += this.parseFragment(frag).replace(/\|---------------$/, '\n');
+							offset = offset.replace(/^\s/, '');
+							break;
+						case 'tbody':
+							data += this.parseFragment(frag);
+							break;
+						case 'thead':
+							data += this.parseFragment(frag);
+							break;
+						// Complex Tables
+						case 'tr':
+							data += this.parseFragment(frag);
+							if (frag.parent.name != 'tr')
+								data += '\n' + offset + '|---------------';
+							break;
+						case 'th':
+							data += '\n' + offset + '{{{#!th' + this.getTableCellAttributes(frag);
+							data += '\n' + this.parseFragment(frag).replace("\n*$", "");
+							data += '\n' + offset + '}}}';
+							break;
+						case 'td':
+							data += '\n' + offset + '{{{#!td' + this.getTableCellAttributes(frag);
+							data += '\n' + this.parseFragment(frag).replace("\n*$", "");
+							data += '\n' + offset + '}}}';
+							break;
+						case 'col':							
+							data+= this.parseFragment(frag);
+							break;
+						case 'colgroup':							
+							data+= this.parseFragment(frag);
+							break;
+						default:
+							data += this.getHtmlBlock(frag);
+						}
+					} else
+					{
+						var value = cleanHtmlEntities( frag.value );
+						if ( !this.programmingStyle && !this.onlyText )
+							value = escapeFormatChars( value );
+						if (frag.parent && frag.parent.name && frag.parent.name.match('blockquote'))
+							data += offset;
+						data += value;
+					}
+				}
+				
+				return data;
+			};
+
+			this.getMarginLeftOffset = function(frag)
+   			{
+				var marginoffset = '';
+				if (frag.attributes.style && frag.attributes.style != null) {
+					var margin = frag.attributes.style.match(marginpattern);
+					if (margin != null)
+						for ( var j = 1; j < margin[1]; j += 40)
+							marginoffset += ' ';
+				}
+				return marginoffset;
+			}
+			
+			this.getTableCellAttributes = function(frag) {
+				var attrib = '';
+				attrib += ' style="' + this.getTableFieldWidth(frag) + this.getTableFieldHeight(frag) + 'background: #' + (frag.name == 'th' ? 'dde' : 'eef')
+						+ '"';
+				attrib += frag.attributes.colspan ? ' colspan=' + frag.attributes.colspan : '';
+				attrib += frag.attributes.rowspan ? ' rowspan=' + frag.attributes.rowspan : '';
+				attrib += frag.attributes.scope ? ' scope=' + frag.attributes.scope : '';
+				attrib += frag.attributes.align ? ' align=' + frag.attributes.align : ' align=justify';
+				attrib = attrib.replace(/undefined/g, '');
+				return attrib;
+			};
+
+			this.getTableFieldHeight = function(frag) {
+				var height = null;
+				if (frag.attributes.style && frag.attributes.style != null)
+					height = frag.attributes.style.match(heightpattern);
+				if (height != null)
+					return height[0] + '; ';
+				else {
+					var columncount = 0, row = frag, table = frag;
+					while (row.name != 'tr' && row.parent.name.match(intablepattern) != null)
+						row = row.parent;
+					if (tableheight != null) {
+						for ( var i = 0; i < row.children.length; i++) {
+							var span = row.children[i].attributes.colspan ? row.children[i].attributes.colspan : 1;
+							columncount += row.children[i].name == 'br' ? 0 : span;
+						}
+						var ownspan = frag.attributes.colspan ? frag.attributes.colspan : 1;
+						height = 'height: ' + ((tableheight[1] / columncount) * ownspan) + '' + tableheight[2] + '; ';
+					}
+				}
+				return (height == null) ? '' : height;
+			};
+
+			this.getTableFieldWidth = function(frag) {
+				var width = null;
+				if (frag.attributes.style && frag.attributes.style != null)
+					width = frag.attributes.style.match(widthpattern);
+				if (width != null)
+					return width[0] + '; ';
+				else {
+					var columncount = 0, row = frag, table = frag;
+					while (row.name != 'tr' && row.parent.name.match(intablepattern) != null)
+						row = row.parent;
+					if (tablewidth != null) {
+						for ( var i = 0; i < row.children.length; i++) {
+							var span = row.children[i].attributes.colspan ? row.children[i].attributes.colspan : 1;
+							columncount += row.children[i].name == 'br' ? 0 : span;
+						}
+						var ownspan = frag.attributes.colspan ? frag.attributes.colspan : 1;
+						width = 'width: ' + ((tablewidth[1] / columncount) * ownspan) + '' + tablewidth[2] + '; ';
+					}
+				}
+				return (width == null) ? '' : width;
+			};
+			
+			this.parseText = function( fragment )
+			{
+				var data = '',
+				 	frag;
+				for (var i = 0; i < fragment.children.length; i++)
+				{
+					frag = fragment.children[i];
+					
+					if ( frag.name == 'pre' && frag.children.length == 1 )
+					{
+						var value = frag.children[0].value;
+						value = value.replace(/\n/g, '[[BR]]');
+						data += value;
+					} else if ( frag.name )
+					{
+						data += this.parseText(frag);
 						switch ( frag.name )
 						{
 							case 'h1':
-								data += '= ' + this.parseFragment(frag) + ' =\n';
-								break;
-							
+							case 'h2':
+							case 'h3':
+							case 'h4':
+							case 'h5':
+							case 'h6':
 							case 'p':
-								data += '\n' + this.parseFragment(frag) + '\n';
-								break;
-							
-							case 'u':
-								data += '__' + this.parseFragment(frag) + '__';
-								break;
-							
-							case 'strong':
-								data += "'''" + this.parseFragment(frag) + "'''";
-								break;
-							
-							case 'em':
-								data += "''" + this.parseFragment(frag) + "''";
-								break;
-								
-							case 'span':
-								if ( frag.attributes.class )
-								{
-									switch ( frag.attributes.class )
-									{
-										case 'underline':
-											data += '__' + this.parseFragment(frag) + "__";
-											break;
-											
-										default:
-											data += this.getHtmlBlock(frag);
-									}
-								}
-								break;
-							
-							case 'ol':
-								list_str = '1. ';
-								data += this.parseFragment(frag);
-								break;
-							
-							case 'ul':
-								list_str = '- ';
-								data += this.parseFragment(frag);
-								break;
-							
-							case 'li':
-								data += list_str + this.parseFragment(frag) + '\n';
-								break;
-							
-							default:
-								data += this.getHtmlBlock(frag);
+							case 'br':
+								// might be that it inserts duplicate break 
+								// when it has an empty p-tag with br-tag
+								// sometimes happens when copying from OO
+								data += '[[BR]]';
 						}
-					}
-					else
+					} else
 					{
-						data += frag.value;
+						var value = cleanHtmlEntities( frag.value );
+						value = escapeFormatChars( value );
+						data += value;
 					}
 				}
-				if ( '!' == data.substr(-1) )
-					data += ' ';
+				
 				return data;
 			};
+
 			
-			var fragment = CKEDITOR.htmlParser.fragment.fromHtml( html, fixForBody );
-			return this.parseFragment(fragment);
+			var fragment = CKEDITOR.htmlParser.fragment.fromHtml(html, fixForBody);
+			var dataProcessor = this.editor.dataProcessor;
+			var wiki = "";
+			// reset vars before parsing
+			this.programmingStyle = null;
+			this.onlyText = null;
+			this.code_lang = '';
+			
+			if ( !dataProcessor.format )
+				wiki = this.parseFragment(fragment)
+			else
+				wiki = this.parseText(fragment);
+			
+			dataProcessor.format = null;
+			this.editor.checkDirty();
+			return wiki;
 		}
 	};
-})();
+})();

trunk/config.js.sample

+/*
+Sample configuration file for CKEditor in conjunction with CKEditorPlugin.
+
+All of these functions and styles are working with current version of 
+CKEditorPlugin (1.1dev).
+*/
+
+CKEDITOR.stylesSet.add( 'custom_styles',
+[
+ 	// see documentation at http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
+	// Block-level styles
+	{ name : 'Normal' , element : 'p', styles : { 'font-weight': 'normal' } },
+	{ name : 'Header 1', element : 'h1' },
+	{ name : 'Header 2' , element : 'h2'},
+ 
+	// Inline styles
+	{name:'yellow marker',element:'span',styles:{'background-color': 'yellow'}},
+	{name:'green marker',element:'span',styles:{'background-color': 'lime'}},
+	{name:'monospaced text',element:'tt'}
+]);
+
+CKEDITOR.editorConfig = function( config )
+{
+	config.toolbar = 'custom';
+	
+	// For full toolbar, see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Toolbar
+	config.toolbar_custom = [
+		['Source', 'Preview','-'],
+		['Cut','Copy','PasteFromWord','PasteCode','-'],
+		['Undo','Redo','-','Replace','-','RemoveFormat'],
+		['Bold','Italic','Underline','Strike','-','Subscript','Superscript'],
+		['NumberedList','BulletedList','-','Outdent','Indent'],
+		'/',
+		['Link','Unlink'],
+		['Image','Table','HorizontalRule','SpecialChar'],
+		['Styles','Format'],
+		['TextColor'],
+		['Maximize', 'ShowBlocks','-','About']
+	];
+	
+	config.stylesSet = 'custom_styles';
+};
 
 setup(
     name = 'CKIntegration',
-    version = '1.0dev',
+    version = '1.1dev',
     description = 'CKEditor integration for Trac',
-    author = 'Itamar Ostricher, Edan Maor',
-    author_email = 'itamarost@gmail.com, edanm@btlms.com',
+    author = 'Itamar Ostricher, Edan Maor, Franz Mayer',
+    author_email = 'itamarost@gmail.com, edanm@btlms.com, franz.mayer@gefasoft.de',
     packages = find_packages(exclude=['*.tests*']),
     package_data = {
-        'ckintegration' : ['htdocs/*.js'],
+        'ckintegration' : ['htdocs/*.js', 'htdocs/images/*.png'],
 
     },
     entry_points = {
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.