Author: CrawfordCurrie Date: 2007-08-09 12:56:26 -0500 (Thu, 09 Aug 2007) New Revision: 14534 Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/data/TWiki/WysiwygPlugin.txt twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin.pm twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/HTML2TML/Node.pm twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/TML2HTML.pm twiki/branches/MAIN/twikiplugins/WysiwygPlugin/test/unit/WysiwygPlugin/TranslatorTests.pm Log: Item4435: at long last I think I've licked the problem of line endings, by using properly structured paragraph blocks in the HTML generator. I believe this points the way ahead for sectional editing too.... Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/data/TWiki/WysiwygPlugin.txt =================================================================== --- twiki/branches/MAIN/twikiplugins/WysiwygPlugin/data/TWiki/WysiwygPlugin.txt 2007-08-09 17:52:59 UTC (rev 14533) +++ twiki/branches/MAIN/twikiplugins/WysiwygPlugin/data/TWiki/WysiwygPlugin.txt 2007-08-09 17:56:26 UTC (rev 14534) @@ -38,17 +38,20 @@ Both translators can be used directly from Perl scripts, for example to build your own stand-alone translators. -A stand-alone convertor script for HTML to TWiki is included in the installation. It can be found in the top-level =tools= directory and is called =html2tml.pl=. +Stand-alone convertor scripts for HTML to TWiki and TWiki to HTML are included in the installation. They can be found in the top-level =tools= directory and is called =html2tml.pl=. ----+++ Integrating a Wysiwyg Editor -The plugin can be used to generate HTML for an editor in two ways; first, by generating the HTML for the content-to-be-edited directly in the edit template, and second, through a URL that can be used to fetch the content-to-be-edited from the server. +---+++ Integrating a HTML Editor +The plugin can be used to integrate an HTML editor in a number of different ways. + 1 The HTML for the content-to-be-edited can be generated directly in the edit template. + 1 A URL can be used to fetch the content-to-be-edited from the server, for use in an IFRAME. + 1 REST handlers can be called from Javacript to convert content. ----++++ Getting content in the edit template +---++++ Generating content directly in the edit template This is the scenario used by the standard TWiki text editor, except that the text is pre-converted to HTML before inclusion in the template. The flow of control is as follows: 1 User hits "edit". - 1 The =beforeEditHandler= filters the edit, blocking any attempt to edit restricted content + 1 The =beforeEditHandler= filters the edit 1 The =edit= template containing the JS editor is instantiated. The following variables are available for expansion in the template: * =%<nop>WYSIWYG_TEXT%= expands to the HTML of the content-to-be-edited. This is suitable for use in a =textarea=. * =%<nop>JAVASCRIPT_TEXT%= expands to the HTML of the content-to-be-edited in a javascript constant. @@ -56,7 +59,7 @@ =WYSIWYGPLUGIN_WYSIWYGSKIN= *must* be set for this to work. ---++++ Fetching content from a URL -In this scenario, the edit template is generated *without* the content-to-be-edited. The content is retrieved from the server using a URL e.g. from an =IFRAME= or using a =XmlHttpRequest=. +In this scenario, the edit template is generated *without* the content-to-be-edited. The content is retrieved from the server using a URL e.g. from an =IFRAME=. The flow of control is as follows: 1 User hits "edit". @@ -86,6 +89,19 @@ * You can pass =noredirect= to the =upload= script to suppress the redirect. In this case you will get a =text/plain= response of =OK= followed by a message if everything went well, or an error message if it did not. +---+++ REST handlers +If you are confident in Javascript you can use REST handlers with =XmlHttpRequest= to convert content from TML to HTML and back again. + +The plugin defines the following REST handlers: + +=.../rest/WysiwygPlugin/html2tml?topic=Web.Topic;text=htmltexttotranslate= + +Converts the HTML text to TML. =topic= *must* be specified. + +=.../rest/WysiwygPlugin/tml2html?topic=Web.Topic;text=tmltexttotranslate= + +Converts the TML text to HTML. =topic= *must* be specified. The response is a =text/plain= page of converted content. + ---++ Plugin Installation Instructions %$INSTALL_INSTRUCTIONS% Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/HTML2TML/Node.pm =================================================================== --- twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/HTML2TML/Node.pm 2007-08-09 17:52:59 UTC (rev 14533) +++ twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/HTML2TML/Node.pm 2007-08-09 17:56:26 UTC (rev 14534) @@ -815,7 +815,7 @@ my( $this, $options ) = @_; my( $f, $kids ) = $this->_flatten( $options ); - return ($f, '<p />'.$kids) if( $options & $WC::NO_BLOCK_TML ); + return ($f, '<p>'.$kids.'</p>') if( $options & $WC::NO_BLOCK_TML ); $kids = _trim($kids); return ($f | $WC::BLOCK_TML, $WC::NBBR.$kids.$WC::NBBR); } @@ -874,6 +874,12 @@ return $this->_PROTECTED($options); } + if ($this->{attrs}->{class} =~ /\bTMLverbatim\b/) { + my( $flags, $text ) = $this->_PROTECTED($options); + my $p = _htmlParams($this->{attrs}, $options, 'TMLverbatim'); + return ($flags, "<verbatim$p>$text</verbatim>"); + } + if( defined( $this->{attrs}->{class} ) && $this->{attrs}->{class} =~ /\bWYSIWYG_NOAUTOLINK\b/ ) { my( $flags, $text ) = $this->_flatten( $options ); @@ -888,7 +894,7 @@ } # ignore all other classes - delete $this->{attrs}->{class}; + #delete $this->{attrs}->{class}; } # ignore the span if there are no attrs Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/TML2HTML.pm =================================================================== --- twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/TML2HTML.pm 2007-08-09 17:52:59 UTC (rev 14533) +++ twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin/TML2HTML.pm 2007-08-09 17:56:26 UTC (rev 14534) @@ -265,99 +265,118 @@ # other entities $text =~ s/&(\w+);/$TT0$1;/g; # "&abc;" $text =~ s/&(#[0-9]+);/$TT0$1;/g; # "{" - #$text =~ s/&/&/g; # escape standalone "&" + #$text =~ s/&/&/g; # escape standalone "&" $text =~ s/$TT0(#[0-9]+;)/&$1/go; $text =~ s/$TT0(\w+;)/&$1/go; - # Headings - # '----+++++++' rule - $text =~ s/$TWiki::regex{headerPatternDa}/_makeHeading($2,length($1))/geom; - # Horizontal rule my $hr = CGI::hr({class => 'TMLhr'}); - $text =~ s/^---+/$hr/gm; + $text =~ s/^---+$/$hr/gm; # Now we really _do_ need a line loop, to process TML # line-oriented stuff. - my $isList = 0; # True when within a list - my $insideTABLE = 0; - my @result = (); + my $inList = 0; # True when within a list type + my $inTable = 0; # True when within a table type + my $inParagraph = 1; # True when within a P + my @result = ( '<p>' ); foreach my $line ( split( /\n/, $text )) { # Table: | cell | cell | # allow trailing white space after the last | if( $line =~ m/^(\s*\|.*\|\s*)$/ ) { - if ($isList) { - $this->_addListItem( \@result, '', '', '' ); - $isList = 0; - } - unless( $insideTABLE ) { + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $this->_addListItem( \@result, '', '', '' ) if $inList; + $inList = 0; + unless( $inTable ) { push( @result, CGI::start_table( { border=>1, cellpadding=>0, cellspacing=>1 } )); } push( @result, _emitTR($1) ); - $insideTABLE = 1; + $inTable = 1; next; - } elsif( $insideTABLE ) { + } + + if( $inTable ) { push( @result, CGI::end_table() ); - $insideTABLE = 0; + $inTable = 0; } - # Lists and paragraphs - if ( $line =~ s/^\s*$/<p \/>/ ) { - $isList = 0; - } - elsif ( $line =~ m/^\S/ ) { - $isList = 0; - } - elsif ( $line =~ m/^(\t| )+\S/ ) { - if ( $line =~ s/^((\t| )+)\$\s(([^:]+|:[^\s]+)+?):\s/<dt> $3 <\/dt><dd> /o ) { - # Definition list - $this->_addListItem( \@result, 'dl', 'dd', $1, '' ); - $isList = 1; + if ($line =~ /$TWiki::regex{headerPatternDa}/o) { + # Running head + $this->_addListItem( \@result, '', '', '' ) if $inList; + $inList = 0; + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $line = _makeHeading($2, length($1)); + + } elsif ($line =~ /^\s*$/) { + # Blank line + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $line = '<p>'; + $this->_addListItem( \@result, '', '', '' ) if $inList; + $inList = 0; + $inParagraph = 1; + + } elsif ( $line =~ s/^((\t| )+)\$\s(([^:]+|:[^\s]+)+?):\s/<dt> $3 <\/dt><dd> /o ) { + # Definition list + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $this->_addListItem( \@result, 'dl', 'dd', $1, '' ); + $inList = 1; + + } elsif ( $line =~ s/^((\t| )+)(\S+?):\s/<dt> $3<\/dt><dd> /o ) { + # Definition list + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $this->_addListItem( \@result, 'dl', 'dd', $1, '' ); + $inList = 1; + + } elsif ( $line =~ s/^((\t| )+)\*(\s|$)/<li> /o ) { + # Unnumbered list + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + $this->_addListItem( \@result, 'ul', 'li', $1, '' ); + $inList = 1; + + } elsif ( $line =~ m/^((\t| )+)([1AaIi]\.|\d+\.?) ?/ ) { + # Numbered list + push(@result, '</p>') if $inParagraph; + $inParagraph = 0; + my $ot = $3; + $ot =~ s/^(.).*/$1/; + if( $ot !~ /^\d$/ ) { + $ot = ' type="'.$ot.'"'; + } else { + $ot = ''; } - elsif ( $line =~ s/^((\t| )+)(\S+?):\s/<dt> $3<\/dt><dd> /o ) { - # Definition list - $this->_addListItem( \@result, 'dl', 'dd', $1, '' ); - $isList = 1; - } - elsif ( $line =~ s/^((\t| )+)\*(\s|$)/<li> /o ) { - # Unnumbered list - $this->_addListItem( \@result, 'ul', 'li', $1, '' ); - $isList = 1; - } - elsif ( $line =~ m/^((\t| )+)([1AaIi]\.|\d+\.?) ?/ ) { - # Numbered list - my $ot = $3; - $ot =~ s/^(.).*/$1/; - if( $ot !~ /^\d$/ ) { - $ot = ' type="'.$ot.'"'; - } else { - $ot = ''; - } - $line =~ s/^((\t| )+)([1AaIi]\.|\d+\.?) ?/<li$ot> /; - $this->_addListItem( \@result, 'ol', 'li', $1, $ot ); - $isList = 1; - } + $line =~ s/^((\t| )+)([1AaIi]\.|\d+\.?) ?/<li$ot> /; + $this->_addListItem( \@result, 'ol', 'li', $1, $ot ); + $inList = 1; + } else { - $isList = 0; + # Other line + $this->_addListItem( \@result, '', '', '' ) if $inList; + $inList = 0; } - # Finish the list - if( ! $isList ) { - $this->_addListItem( \@result, '', '', '' ); - $isList = 0; - } - push( @result, $line ); } - if( $insideTABLE ) { + if( $inTable ) { push( @result, '</table>' ); + } elsif ($inList) { + $this->_addListItem( \@result, '', '', '' ); + } elsif ($inParagraph) { + push(@result, '</p>'); } - $this->_addListItem( \@result, '', '', '' ); $text = join("\n", @result ); + # Trim any extra Ps from the top and bottom. + $text =~ s#^(\s*<p>\s*</p>)+##s; + $text =~ s#(<p>\s*</p>\s*)+$##s; + $text =~ s(${STARTWW}==([^\s]+?|[^\s].*?[^\s])==$ENDWW) (CGI::b(CGI::code($1)))gem; $text =~ s(${STARTWW}__([^\s]+?|[^\s].*?[^\s])__$ENDWW) @@ -404,7 +423,7 @@ $this->_putBackBlocks( $text, 'pre' ); - # replace verbatim with pre in the final output + # replace verbatim with pre in the final output, with encoded entities $this->_putBackBlocks( $text, 'verbatim', 'pre', \&_encodeEntities ); $text =~ s/(<nop>)/$this->_liftOut($1, 'PROTECTED')/ge; Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin.pm =================================================================== --- twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin.pm 2007-08-09 17:52:59 UTC (rev 14533) +++ twiki/branches/MAIN/twikiplugins/WysiwygPlugin/lib/TWiki/Plugins/WysiwygPlugin.pm 2007-08-09 17:56:26 UTC (rev 14534) @@ -22,26 +22,8 @@ This plugin is responsible for translating TML to HTML before an edit starts and translating the resultant HTML back into TML. -The flow of control is as follows: - 1 User hits "edit" - 2 if the skin is WYSIWYGPLUGIN_WYSIWYGSKIN, the beforeEditHandler - filters the edit - 3 The 'edit' template is instantiated with all the js and css - * JS, WYSIWYG and plain text formats are available for embedding - the content into the template, if that's what the editor requires. - 4 editor JS starts up and optionally invokes view URL with the - 'wysiwyg_edit=1' parameter to obtain the clean document (if it wasn't - provided in the template) - * The beforeCommonTagsHandler is implemented by the plugin in this - mode. This handler formats the text and then stores it away so the rest - of twiki rendering can't do anything to it. In the postRenderingHandler - it drops the stored text back into the template. - 5 User edits - 6 editor saves by posting to 'save' with the 'wysiwyg_edit=1' parameter. - The beforeSaveHandler sees this and converts the HTML back to TML. - Note: In the case of a new topic, you might expect to see the "create topic" -screen in the editor when it goesback to twiki for the topic content. This +screen in the editor when it goes back to twiki for the topic content. This doesn't happen because the earliest possible handler is called on the topic content and not the template. The template is effectively ignored and a blank document is sent to the editor. @@ -86,6 +68,9 @@ TWiki::Func::registerTagHandler('WYSIWYG_TEXT',\&_WYSIWYG_TEXT); TWiki::Func::registerTagHandler('JAVASCRIPT_TEXT',\&_JAVASCRIPT_TEXT); + TWiki::Func::registerRESTHandler('tml2html', \&_restTML2HTML); + TWiki::Func::registerRESTHandler('html2tml', \&_restHTML2TML); + # Plugin correctly initialized return 1; } @@ -527,4 +512,37 @@ ); } +# Rest handler for use from Javascript +sub _restTML2HTML { + my ($session) = @_; + my $tml = TWiki::Func::getCgiQuery()->param('text'); + my $html = TranslateTML2HTML( + $tml, $session->{webName}, $session->{topicName} ); + print CGI::header('text/plain'); + print $html; +} + +# Rest handler for use from Javascript +sub _restHTML2TML { + my ($session) = @_; + unless( $html2tml ) { + require TWiki::Plugins::WysiwygPlugin::HTML2TML; + + $html2tml = new TWiki::Plugins::WysiwygPlugin::HTML2TML(); + } + my $html = TWiki::Func::getCgiQuery()->param('text'); + my $tml = $html2tml->convert( + $html, + { + web => $session->{webName}, + topic => $session->{topicName}, + getViewUrl => \&getViewUrl, + expandVarsInURL => \&expandVarsInURL, + very_clean => 1, + }); + print CGI::header('text/plain'); + print $tml; +} + 1; + Modified: twiki/branches/MAIN/twikiplugins/WysiwygPlugin/test/unit/WysiwygPlugin/TranslatorTests.pm =================================================================== --- twiki/branches/MAIN/twikiplugins/WysiwygPlugin/test/unit/WysiwygPlugin/TranslatorTests.pm 2007-08-09 17:52:59 UTC (rev 14533) +++ twiki/branches/MAIN/twikiplugins/WysiwygPlugin/test/unit/WysiwygPlugin/TranslatorTests.pm 2007-08-09 17:56:26 UTC (rev 14534) @@ -43,11 +43,10 @@ my $mask = $TML2HTML | $HTML2TML | $ROUNDTRIP; my $protecton = '<span class="WYSIWYG_PROTECTED">'; -my $preon = '<pre class="WYSIWYG_PROTECTED">'; my $linkon = '<span class="WYSIWYG_LINK">'; my $protectoff = '</span>'; my $linkoff = '</span>'; -my $preoff = '</pre>'; +my $preoff = '</span>'; my $nop = '<nop>'; # The following big table contains all the testcases. These are @@ -160,7 +159,7 @@ exec => $ROUNDTRIP, name => 'mixtureOfFormats', html => <<'HERE', -<i>this</i><i>should</i><i>italicise</i><i>each</i><i>word</i><p /><b>and</b><b>this</b><b>should</b><b>embolden</b><b>each</b><b>word</b><p /><i>mixing</i><b>them</b><i>should</i><b>work</b> +<p><i>this</i><i>should</i><i>italicise</i><i>each</i><i>word</i><p /><b>and</b><b>this</b><b>should</b><b>embolden</b><b>each</b><b>word</b></p><p><i>mixing</i><b>them</b><i>should</i><b>work</b></p> HERE tml => <<'HERE', _this_ _should_ _italicise_ _each_ _word_ @@ -174,7 +173,7 @@ exec => $ROUNDTRIP, name => 'simpleVerbatim', html => <<'HERE', -<pre class="TMLverbatim"><br /><verbatim><br />Description<br /></verbatim><br />class CatAnimal {<br /> void purr() {<br /> code <here><br /> }<br />}<br /></pre> +<span class="TMLverbatim"><br /><verbatim><br />Description<br /></verbatim><br />class CatAnimal {<br /> void purr() {<br /> code <here><br /> }<br />}<br /></span> HERE tml => <<'HERE', <verbatim> @@ -407,7 +406,7 @@ exec => $ROUNDTRIP, name => 'mailtoLink2', html => ' a@z.com ', - tml => ' a@z.com', + tml => 'a@z.com', }, { exec => $ROUNDTRIP, @@ -634,7 +633,7 @@ exec => $ROUNDTRIP, name => 'nestedVerbatim', html => 'Outside - <pre class="TMLverbatim"><br />Inside<br /></pre>Outside', + <span class="TMLverbatim"><br />Inside<br /></span>Outside', tml => 'Outside <verbatim> Inside @@ -647,9 +646,8 @@ { exec => $TML2HTML | $ROUNDTRIP, name => 'nestedPre', - html => 'Outside - <pre class="twikiAlert TMLverbatim"><br /> Inside<br /> </pre> - Outside', + html => '<p> +Outside <pre class="twikiAlert TMLverbatim"><br /> Inside<br /> </pre> Outside </p>', tml => 'Outside <verbatim class="twikiAlert"> Inside </verbatim> Outside', @@ -657,7 +655,7 @@ { exec => $ROUNDTRIP, name => 'nestedIndentedVerbatim', - html => 'Outside<pre class="TMLverbatim"><br />Inside<br /> </pre>Outside', + html => 'Outside<span class="TMLverbatim"><br />Inside<br /> </span>Outside', tml => 'Outside <verbatim> Inside @@ -689,18 +687,12 @@ exec => $HTML2TML | $ROUNDTRIP, name => 'classifiedPre', html => 'Outside - <pre class="twikiAlert"> + <span class="twikiAlert"> Inside - </pre> + </span> Outside', - tml => 'Outside -<pre class="twikiAlert"> - Inside - </pre> -Outside', - finaltml => 'Outside <pre class="twikiAlert"> - Inside - </pre> Outside', + tml => 'Outside <span class="twikiAlert"> Inside </span> Outside', + finaltml => 'Outside <span class="twikiAlert"> Inside </span> Outside', }, { exec => $ROUNDTRIP, @@ -833,7 +825,7 @@ 5 <span class="arfle" lang="fr">francais</span> HERE tml => <<HERE, -1 2 3 4 francais 5 francais +1 <span class="arfle" /> 2 3 4 francais 5 <span class="arfle">francais</span> HERE }, { @@ -1031,13 +1023,17 @@ { name => 'paraConversions1', exec => $TML2HTML | $HTML2TML | $ROUNDTRIP, - html => 'Paraone + html => '<p> +Paraone Paratwo -<p /> +</p> +<p> Parathree -<p /> -<p /> -Parafour', +</p> +<p></p> +<p> +Parafour +</p>', tml => 'Paraone Paratwo |