# LatexModePlugin.pm # Copyright (C) 2005 W Scott Hoge, shoge at bwh dot harvard dot edu # Copyright (C) 2002 Graeme Lufkin, gwl@u.washington.edu # # TWiki WikiClone ($wikiversion has version info) # # Copyright (C) 2000-2001 Andrea Sterbini, a.sterbini@flashnet.it # Copyright (C) 2001 Peter Thoeny, Peter@Thoeny.com # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details, published at # http://www.gnu.org/copyleft/gpl.html # # ========================= # # This is the Math Mode TWiki plugin. See TWiki.LatexModePlugin for details. # # Each plugin is a package that contains the subs: # # initPlugin ( $topic, $web, $user, $installWeb ) # commonTagsHandler ( $text, $topic, $web ) # outsidePREHandler ( $text ) # insidePREHandler ( $text ) # postRenderingHandler ( $text ) # # initPlugin is required, all other are optional. # For increased performance, all handlers except initPlugin are # disabled. To enable a handler remove the leading DISABLE_ from # the function name. # # NOTE: To interact with TWiki use the official TWiki functions # in the &TWiki::Func module. Do not reference any functions or # variables elsewhere in TWiki!! # LatexModePlugin: This plugin allows you to include mathematics and # other Latex markup commands in TWiki pages. To declare a portion of # the text as latex, enclose it within any of the available markup tags: # %$ ... $% for in-line equations # %\[ ... \]% or # %MATHMODE{ ... }% for own-line equations # # For multi-line, or more complex markup, the syntax # %BEGINLATEX{}% ... %ENDLATEX% is also available. # # An image is generated for each latex expression on a page by # generating an intermediate PostScript file, and then using the # 'convert' command from ImageMagick. The rendering is done the first # time an expression is used. Subsequent views of the page will not # require a re-render. Images from old expressions no longer included # in the page will be deleted. # ========================= package TWiki::Plugins::LatexModePlugin; use strict; # ========================= use vars qw( $web $topic $user $installWeb $VERSION $RELEASE $debug $default_density $default_gamma $default_scale $preamble $eqn $fig $tbl $use_color @EXPORT_OK ); # number the release version of this plugin $VERSION = '2.3'; $RELEASE = 'Dakar'; require Exporter; *import = \&Exporter::import; @EXPORT_OK = qw($preamble); #the MD5 hash function is used to uniquely identify a string of math use Digest::MD5 qw( md5_hex ); #we use the basename() function to determine which script is running use File::Basename qw( basename ); use File::Copy qw( move copy ); use File::Temp; # use Image::Info to identify image size. use Image::Info qw( image_info ); ###################################################################### ### installation specific variables: my $pathSep = ($^O =~ m/^Win/i) ? "\\" : '/' ; my $PATHTOLATEX = $TWiki::cfg{Plugins}{LatexModePlugin}{latex} || '/usr/bin/latex'; my $PATHTODVIPS = $TWiki::cfg{Plugins}{LatexModePlugin}{dvips} || '/usr/bin/dvips'; my $PATHTOCONVERT = $TWiki::cfg{Plugins}{LatexModePlugin}{convert} || '/usr/X11R6/bin/convert'; my $GREP = $TWiki::cfg{Plugins}{LatexModePlugin}{fgrep} || $TWiki::fgrepCmd || '/usr/bin/fgrep'; ### The variables below this line will likely not need to be changed ###################################################################### # this is the extension of the generated images. gif or jpg are other # possibilities. my $EXT = 'png'; #this is the name of the latex file created by the program. You shouldn't #need to change it unless for some bizarre reason you have a file attached to #a TWiki topic called twiki_math or twiki_math.tex my $LATEXBASENAME = 'twiki_math'; my $LATEXFILENAME = $LATEXBASENAME . '.tex'; #this variable gives the length of the hash code. If you switch to a different #hash function, you will likely have to change this my $HASH_CODE_LENGTH = 32; #this hash table will contain the math strings, indexed by their hash code my %hashed_math_strings = (); # this hash table is used to store declared markup options # to be used during rendering (e.g. in-line vs. own-line equations) my %markup_opts = (); # these store the numbers for the my %eqnrefs = (); # equation back-references my %figrefs = (); # figure back-references my %tblrefs = (); # table back-references # a place to store intermediate errors until all latex handling is done. my $error_catch_all = ""; #the url to the attachment directory for this page my $pubUrlPath; # get the name of the script that called us my $script = basename( $0 ); ### the output of each function depends on whether the output is HTML ### based or destined for pdflatex processing. This flag selects ### between the two. my $latexout = 0 ; # ========================= sub initPlugin { ( $topic, $web, $user, $installWeb ) = @_; # check for Plugins.pm versions if( $TWiki::Plugins::VERSION < 1.025 ) { # this version is Dakar and Cairo compatible &TWiki::Func::writeWarning( "Version mismatch between LatexModePlugin (Dakar edition) and Plugins.pm" ); return 0; } #get the relative URL to the attachment directory for this page $pubUrlPath = # &TWiki::Func::getUrlHost() . &TWiki::Func::getPubUrlPath() . "/$web/$topic"; # Get preferences values $debug = &TWiki::Func::getPreferencesFlag( "LATEXMODEPLUGIN_DEBUG" ); $default_density = &TWiki::Func::getPreferencesValue( "DENSITY" ) || &TWiki::Func::getPreferencesValue( "LATEXMODEPLUGIN_DENSITY" ) || 116; $default_gamma = &TWiki::Func::getPreferencesValue( "GAMMA" ) || &TWiki::Func::getPreferencesValue( "LATEXMODEPLUGIN_GAMMA" ) || 0.6; $default_scale = &TWiki::Func::getPreferencesValue( "SCALE" ) || &TWiki::Func::getPreferencesValue( "LATEXMODEPLUGIN_SCALE" ) || 1.0; $preamble = &TWiki::Func::getPreferencesValue( "PREAMBLE" ) || &TWiki::Func::getPreferencesValue( "LATEXMODEPLUGIN_PREAMBLE" ) || '\usepackage{latexsym}'."\n"; # initialize counters # Note, these can be over-written by topic declarations $eqn = &TWiki::Func::getPreferencesValue( "EQN" ) || 0; $fig = &TWiki::Func::getPreferencesValue( "FIG" ) || 0; $tbl = &TWiki::Func::getPreferencesValue( "TBL" ) || 0; $use_color = 0; # initialize color setting. $latexout = 1 if ($script =~ m/genpdflatex/); # Plugin correctly initialized &TWiki::Func::writeDebug( "- TWiki::Plugins::LatexModePlugin::initPlugin( $web.$topic ) is OK" ) if $debug; return 1; } sub commonTagsHandler { ### my ( $text, $topic, $web ) = @_; # do not uncomment, use $_[0], $_[1]... instead TWiki::Func::writeDebug( " TWiki::Plugins::LatexModePlugin::commonTagsHandler( $_[2].$_[1] )" ) if $debug; # This is the place to define customized tags and variables # Called by sub handleCommonTags, after %INCLUDE:"..."% # handle floats first, in case of latex markup in captions. $_[0] =~ s!%BEGINFIGURE{(.*?)}%(.*?)%ENDFIGURE%!&handleFloat($2,$1,'fig')!giseo; $_[0] =~ s!%BEGINTABLE{(.*?)}%(.*?)%ENDTABLE%!&handleFloat($2,$1,'tbl')!giseo; ### handle the standard syntax next $_[0] =~ s/%(\$.*?\$)%/&handleLatex($1,'inline="1"')/gseo; $_[0] =~ s/%(\\\[.*?\\\])%/&handleLatex($1,'inline="0"')/gseo; $_[0] =~ s/%MATHMODE{(.*?)}%/&handleLatex("\\[".$1."\\]",'inline="0"')/gseo; # pass everything between the latex BEGIN and END tags to the handler # $_[0] =~ s!%BEGINLATEX{(.*?)}%(.*?)%ENDLATEX%!&handleLatex($2,$1)!giseo; $_[0] =~ s!%BEGINLATEX%(.*?)%ENDLATEX%!&handleLatex($1,'inline="0"')!giseo; $_[0] =~ s!%BEGINLATEXPREAMBLE%(.*?)%ENDLATEXPREAMBLE%!&handlePreamble($1)!giseo; #Custom $_[0] =~ s!%SECTIONSTART\((\d+)\)%!&handleSectionStart($1)!iseo; $_[0] =~ s!%USEMACROPAGE\((.*?)\)%!&handleMacroPage($1)!iseo; $_[0] =~ s!%BEGINALLTEX%(.*?)%ENDALLTEX%!&handleAlltex($1)!giseo; # last, but not least, replace the references to equations with hyperlinks $_[0] =~ s!%REFLATEX{(.*?)}%!&handleReferences($1)!giseo; } # ========================= sub handlePreamble { my $text = $_[0]; $preamble .= $text; return(''); } # ========================= sub handleReferences { # This function converts references to defined # equations/figures/tables and replaces them with the Eqn/Fig/Tbl # number ### my ( $math_string ) = @_; # do not uncomment, use $_[0], $_[1] instead my $ref = $_[0]; my ($backref,$txt) = ("",""); if ($latexout) { $txt = '\ref{'.$ref.'}'; } else { if ($ref=~m/^tbl\:/) { $backref = exists($tblrefs{$ref}) ? $tblrefs{$ref} : "?? REFLATEX{$ref} not defined in table list ??"; } elsif ($ref=~m/^fig\:/) { $backref = exists($figrefs{$ref}) ? $figrefs{$ref} : "?? REFLATEX{$ref} not defined in fig list ??"; $txt = ''.$backref.''; } else { if (exists($eqnrefs{$ref})) { $backref = $eqnrefs{$ref}; } elsif (exists($eqnrefs{ "eqn:".$ref })) { $backref = $eqnrefs{ "eqn:".$ref }; } else { $backref = "?? REFLATEX{$ref} not defined in eqn list ??"; } $txt = '('.$backref.')'; } } return($txt); } # ========================= sub handleFloat { # This function mimics the construction of float environments in latex, # producing a back-reference list for Figures and Tables. ### my ( $input ) = @_; # do not uncomment, use $_[0], $_[1] instead my $input = $_[0]; my $prefs = $_[1]; my @a=('0'..'9','a'..'z','A'..'Z'); my $str = map{ $a[ int rand @a ] } (0..7); my %opts = ( 'label' => $str, 'span' => 'onecol', 'caption' => ' ' ); my %opts2 = TWiki::Func::extractParameters( $prefs ); map { $opts{$_} = $opts2{$_} } keys %opts2; # while ( $prefs=~ m/(.*?)=\"(.*?)\"/g ) { # my ($a,$b) = ($1,$2); # # remove leading/trailing whitespace from key names # $a =~ s/^\s*|\s*$//; # # $opts{$a} = $b; # } my $env = ($_[2] eq 'fig') ? "Figure" : "Table" ; my $tc = ($opts{'span'} =~ m/^twoc/) ? '*' : '' ; # ensure that the first 4 chars of the label conform to # 'fig:' or 'tbl:' or ... ( $opts{'label'} = $_[2].":".$opts{'label'} ) unless ( substr($opts{'label'},0,4) eq $_[2].':' ); my $txt2 = ""; if( $latexout ) { ## for genpdflatex # in Cairo (at least) latex new-lines, '\\', get translated to # spaces, '\', but if they appear at the end of the line. # So pad in a few spaces to protect them... $input =~ s!\n! \n!g; $txt2 = ''; $txt2 .= "\n\\begin{".lc($env).$tc."}\\centering\n"; $txt2 .= $input."\n\\caption{".$opts{'caption'}."}\n"; $txt2 .= '\label{'.$opts{'label'}."}\n\\end{".lc($env).$tc."}"; $txt2 .= ''; } else { ## otherwise, generate HTML ... my $infrmt = '%s'; my $cpfrmt = ' *%s %d*: %s'; if ($_[2] eq 'fig') { $fig++; $txt2 .= sprintf($infrmt."\n",$input). sprintf($cpfrmt."\n",$env,$fig,$opts{'caption'}); my $key = $opts{'label'}; $figrefs{$key} = $fig; } elsif ($_[2] eq 'tbl') { $tbl++; $txt2 .= sprintf($cpfrmt."\n",$env,$fig,$opts{'caption'}). sprintf($infrmt."\n",$input); my $key = $opts{'label'}; $tblrefs{$key} = $tbl; } else { $txt2 .= $input; } $txt2 = '' . ''."\n" . $txt2 . '
'; } # end. if !($latexout) return($txt2); } # ========== my $sec_num = 0; my $subsec_num = 0; my $def_num = 0; my $ext_macro = 0; my %texhash; my %thmhash; my $thm_autonumber = 0; sub handleSectionStart { $sec_num = $_[0] - 1; return ""; } sub handleMacroPage { my $web; my $topic; my $meta; my $text; $meta = $_[0]; $meta =~ m!^(.*)\.(.*)$!; $web = $1; $topic = $2; $ext_macro = 1; ($meta, $text) = TWiki::Func::readTopic($web,$topic); $text =~ s!%BEGINMACRO%(.*?)%ENDMACRO%!&handlePreamble($1)!iseo; return ""; } sub handleAlltex { my $txt = ''; my $math_string = $_[0]; #get rid of comments # % ... \n $math_string =~ s!%.*?\n!!gis; #everything between \documentclass and \begin{doc..} is preamble $math_string =~ m!\\documentclass\s*\{.*?\}\s*(\[.*?\])?(.*?)\\begin\s*\{document\}\s*(.*?)\s*\\end\s*\{document\}!is; #$1 = junk #$2 = preamble #$3 = document my $pre = $2; my $doc = $3; handlePreamble($pre) if (!$ext_macro); $pre = $preamble; #parse preamble and set up theorem numbering #first find the section-linked thm (\newtheorem{$envname}{$type}[section]) if($pre =~ m!\\newtheorem\{(.*?)\}\{(.*?)\}\[section\]!i) { #$1 = theorem env name #$2 = theorem type my $thm_envname = $1; my $thm_type = $2; my $thm_maintype = $1; $thmhash{$thm_envname} = $thm_type; $thm_autonumber = 1; #now find everything else that is associated with it # \newtheorem{$envname}[$thm_maintype]{$type} $thm_maintype = quotemeta($thm_maintype); # $txt .= "$thm_maintype \n $pre \n"; while ($pre =~ m!\\newtheorem\s*\{(.*?)\}\[$thm_maintype\]\{(.*?)\}!i) { #$1 = env name #$2 = thm type $thm_envname = $1; $thm_type = $2; $thmhash{$thm_envname} = $thm_type; $thm_envname = quotemeta($thm_envname); $thm_type = quotemeta($thm_type); $pre =~ s!\\newtheorem\{$thm_envname\}\[$thm_maintype\]\{$thm_type\}!!i; # $txt .= "$thm_envname => $thm_type\n"; } } #change \newpage to
$doc =~ s!\\newpage!
!gis; #NOW handled in the bottom part #enumerates first (so that begin doesn't do it later) #$doc =~ s!\\(begin|end)\{enumerate\}!!gis; #$doc =~ s!\\item\S*!1\.!gis; #change all $$'s to begin maths! $doc =~ s!\\\[(.*?)\\\]!\\begin\{displaymath\} $1 \\end\{displaymath\}!gis; $doc =~ s!\$\$(.*?)\$\$!\\begin\{displaymath\} $1 \\end\{displaymath\}!gis; # $doc =~ s!\\\((.*?)\\\)!\\begin\{math\} $1 \\end\{math\}!gis; $doc =~ s!\$(.*?)\$!\\begin\{math\} $1 \\end\{math\}!gis; # return $doc; #new approach! find all begins of course and latex 'em: my($block); #resuse $pre for beginning #(\\begin\s*\{.*?\})|\$\$|\\\[) #in single quotes \\ => \ ($pre,$block,$doc) = umbrellaHook($doc,'\\\\begin\s*\{.*?\}','\\\\end\s*\{.*?\}'); while(!($block eq '')) { #now process the block! #special case if block name = math! #special case if theorem. $block =~ m!^\\begin\{(.*?)\}!si; my $bname = $1; if($bname eq 'math') { $pre .= handleLatex($block,'inline="1"'); } elsif($thm_autonumber && defined($thmhash{$bname})) { #number is sec_num . def_num $bname = quotemeta($bname); my $thmtxt = $thmhash{$bname}; if($pre =~ m!\\section!i) { $def_num = -1; #little kluge to warn commonScan to set to 1 next time. $thmtxt .= " " . ($sec_num + 1) . ".1"; } else { $def_num++; $thmtxt .= " $sec_num\.$def_num"; } $block =~ s!^\\begin\{$bname\}\s*(.*?)\s*\\end\{$bname\}$!$1!is; if($block =~ m!^\[(.*)\]!) { #put that with the theorem name, handle latex if necesary $thmtxt .= " ($1)"; $thmtxt =~ s!\\begin\{math\}\s*(.*?)\s*\\end\{math\}!\$$1\$!gis; $thmtxt =~ s/(\$.*?\$)/&handleLatex($1,'inline="1"')/gseo; $block =~ s!^\[(.*)\]!!; } $pre .= "
\n---+++++ $thmtxt"; $pre .= handleLatex($block,'inline="0"'); $pre .= "
\n"; } else { #set everything in here back to $$ #$doc =~ s!\$(.*?)\$!\\begin\{math\} $1 \\end\{math\}!gis; #$doc =~ s!\\\[(.*?)\\\]!\\begin\{displaymath\} $1 \\end\{displaymath\}!gis; $pre .= replaceTextHiders(0,$block); #$block =~ s!\\begin\{math\}\s*(.*?)\s*\\end\{math\}!\$$1\$!gis; #$block =~ s!\\begin\{displaymath\}\s*(.*?)\s*\\end\{displaymath\}!\\\[$1\\\]!gis; #$pre .= handleLatex($block,'inline="0"'); } ($pre,$doc) = commonScan($pre, $doc); #put pre into txt $txt .= $pre; #next! ($pre,$block,$doc) = umbrellaHook($doc,'\\\\begin\s*\{.*?\}','\\\\end\s*\{.*?\}'); } #there is still some $doc left: ($pre,$doc) = commonScan($doc); $txt .= $pre; # replace the simple math constructs # $doc =~ s!\$\$\s*(.*?)\s*\$\$!&handleLatex('$$' . $1 . '$$','inline="0"')!giseo; # $doc =~ s!\\\[\s*(.*?)\s*\\\]!&handleLatex('\[' . $1 . '\]','inline="0"')!giseo; #we are done! return($txt); } #handle nested texthiders #an itemizer block has \item elements in it. #proof blocks... #verbatims... (handle when necesary) #return replacement of a test masking block sub replaceTextHiders { # $_[0] = depth of itemizer.. starting depth is 0 # - only increment depth when \item is first encountered! # $_[1] = blk my $depth = $_[0]; my $blk = $_[1]; my $atomic = 1; #whether it should be diverted to latex imager #first check #first go through and replace all inner text hiders my ($pre, $chk, $tmp); $tmp = $blk; ($pre, $chk, $tmp) = umbrellaHook($tmp, '\\\\begin\s*\{.*?\}', '\\\\end\s*\{.*?\}'); #if it is a math environment... ignore the next stuff (e.g. do not go into it) if(!($chk =~ m!^\\begin\s*\{math\}!i || $chk =~ m!^\\begin\s*\{displaymath\}!i || $chk =~ m!^\\begin\s*\{eqnarray\*?\}!i || $chk =~ m!^\\begin\s*\{equation\*?\}!i ) ) { #if it matches the entire thing... chop it down to size. if($chk eq $blk) { $chk =~ s!^\\begin\s*\{.*?\}!!; $chk =~ s!\\end\s*\{.*?\}$!!; ($pre, $chk, $tmp) = umbrellaHook($chk, '\\\\begin\s*\{.*?\}', '\\\\end\s*\{.*?\}'); } #if pre has an \\item, increment depth #also signal that it is not atomic (will not be diverted to latex) if(!($chk eq '')) { if($pre =~ m!\\item!) { #its an itemizer.. do a $blk = ""; $depth++; $atomic = 0; } } else { if($blk =~ m!\\item!) { $blk = ""; $depth++; $atomic = 0; } } my $repl; # TWiki::Func::writeDebug("replaceTextHiders - depth: $depth, blk: $blk"); while(!($chk eq '')) { #recurse on texthider $repl = replaceTextHiders($depth, $chk); $chk = quotemeta($chk); $blk =~ s!$chk!$repl!; ($pre, $chk, $tmp) = umbrellaHook($tmp, '\\\\begin\s*\{.*?\}', '\\\\end\s*\{.*?\}'); } } #everything has been replaced and recursed.. now to sweep up the rest #any \\items here will be replaced with \n (depth*3 spaces) * #any \begin{proof} will be replaced with \n *Proof:* \n ... #any \end{proof} will be replaced with *End Proof* #any other \begin and \ends are just removed and ignored for now... #add more non-atomic sections $atomic = 0 if($blk =~ m!^\\begin\s*\{proof\}!i || 0 ); if($atomic) { #if it is math, then inline it if($blk =~ m!^\\begin\s*\{math\}!i) { $blk = handleLatex($blk,'inline="1"'); } else { #blk most likely has images. TIME TO REPLACE THEM ALL! #also remove any div align centers.. any html tags at all # my $imgchk; #

\"$escaped\"

$blk =~ s!

!!ig; $blk =~ s!

!!ig; my $timg; my $tihash; my $timstring; while($blk =~ m!()!is) { $timg = quotemeta($1); $tihash = $2; $timstring = $texhash{$tihash}; $blk =~ s!$timg!$timstring!is; } $blk =~ s!\\begin\{math\}\s*(.*?)\s*\\end\{math\}!\$$1\$!gis; $blk =~ s!\\begin\{displaymath\}\s*(.*?)\s*\\end\{displaymath\}!\\\[$1\\\]!gis; $blk = handleLatex($blk,'inline="0"'); } } else { # my $spc = ""; # for(my $i = 0; $i < $depth; $i++) { # $spc .= ' '; # } # TWiki::Func::writeDebug("replaceTextHiders - depth: $depth, blk: $blk, \n ####amount of space: #$spc#"); $blk =~ s!\\item!\n

  • !gi; $blk =~ s!\\begin\s*\{proof\}!\nProof: \t!gi; $blk =~ s!\\end\s*\{proof\}!\n ||| \n!gi; $blk =~ s!\\begin\s*\{.*?\}!!gi; $blk =~ s!\\end\s*\{.*?\}!!gi; } # TWiki::Func::writeDebug("Final replacement: -depth: $depth, OUTPUT: $blk"); return $blk; } #really badly coded scanner sub commonScan { #scan for the common stuff and replace them my $pre = $_[0]; #the main part my $post = ''; $post = $_[1] if (defined($_[1])); #optional next part. #replace \section data one at a time: my $secsearch = $pre; my ($sty,$src,$t1,$blk,$t2); while($secsearch =~ m!\\((section|subsection|subsubsection)\*?)\s*(\{.*)$!is) { #$1 = section/subsection #ignore $2 #$3 = rest, to be used with hook $sty = $1; $secsearch = $3; ($t1,$blk,$t2) = umbrellaHook($secsearch,'\{','\}'); if($blk eq '') { if(!($post eq '')) { my $prepart = $t2; #t2 now has everything from the pre part my $postpart; ($t1,$blk,$t2) = umbrellaHook($secsearch . $post, '\{','\}'); if($blk eq '') { die("Error: mismatched curly brackets around:\n $secsearch"); } #separate the two parts. $prepart = quotemeta($prepart); $blk =~ m!$prepart(.*)$!is; $postpart = $1; #remove postpart from the ending (make sure to return this optional return val!) $postpart = quotemeta($postpart); $post =~ s!^$postpart!!is; # return "post: $post"; #replace prepart rather than blk now! my $numplus; if($sty eq 'section') { $sec_num++; $subsec_num = 0; if($def_num == -1) { #see handleAlltex for details. $def_num = 1; } else { $def_num = 0; } $numplus = "+"; } else { $subsec_num++; $numplus = "+++"; } my $sec_str = '';; if($sty eq 'section') { $sec_str = "$sec_num"; } elsif($sty eq 'subsection') { $sec_str = "$sec_num" . '.' . "$subsec_num"; } else { $sec_str = ""; } $blk =~ m!^\{(.*)\}$!is; $blk = $1; my $tqprepart = $prepart; #already quoted $sty = quotemeta($sty); #account for * # return "agggh: $tqprepart :: $blk"; #handle math $blk =~ s!\\begin\{math\}\s*(.*?)\s*\\end\{math\}!\$$1\$!gis; $blk =~ s/(\$.*?\$)/&handleLatex($1,'inline="1"')/gseo; $pre =~ s!\s*\\$sty\s*$tqprepart!\n\n\-\-\-$numplus $sec_str $blk\n!is; last; #return to next part! } else { die("Error: mismatched curly brackets around:\n $secsearch"); } } $secsearch = $t2; my $numplus; if($sty eq 'section') { $sec_num++; $subsec_num = 0; if($def_num == -1) { #see handleAlltex for details. $def_num = 1; } else { $def_num = 0; } $numplus = "+"; } else { $subsec_num++; $numplus = "+++"; } my $sec_str = ''; if($sty eq 'section') { $sec_str = "$sec_num"; } elsif($sty eq 'subsection') { $sec_str = "$sec_num" . '.' . "$subsec_num"; } else { $sec_str = ""; } $blk =~ m!^\{(.*)\}$!is; $blk = $1; $sty = quotemeta($sty); my $tqblk = quotemeta($blk); #handle math $blk =~ s!\\begin\{math\}\s*(.*?)\s*\\end\{math\}!\$$1\$!gis; $blk =~ s/(\$.*?\$)/&handleLatex($1,'inline="1"')/gseo; $pre =~ s!\s*\\$sty\s*\{$tqblk\}!\n\n\-\-\-$numplus $sec_str $blk\n!is; } # $secsearch = $pre; =specitbold while($pre =~ m!\\(textit|textbf)\s*(\{.*\})!is) { $sty = $1; $src = $2; ($t1,$blk,$t2) = umbrellaHook($src,'\{','\}'); #fails if $blk eq '' if($blk eq '') { last; } # return "t1: $t1 ::: blk: $blk ::: t2: $t2 \n"; $blk =~ m!^\{(.*)\}$!is; $blk = $1; my $qblk = quotemeta($blk); # return "sty: $sty ::: blk: $blk"; if($sty eq 'textit') { $pre =~ s!\\$sty\s*\{$qblk\}!\_$blk\_!is; } else { $pre =~ s!\\$sty\s*\{$qblk\}!\*$blk\*!is; } } =cut #some formats to handle # { ... } # \asdf { ... } $secsearch = $pre; $pre = ''; #reaccum. #don't do anything if it is caught within tag #fixed by removing things from $escaped in handleLatex while($secsearch =~ m!(\\\S*)?\s*(\{.*)$!is) { $pre .= $`; $sty = $1; $src = $2; ($t1,$blk,$t2) = umbrellaHook($src,'\{','\}'); if($blk eq '') { if($post eq '') { last; } else { my $prepart = $t2; #t2 now has everything from the pre part my $postpart; ($t1,$blk,$t2) = umbrellaHook($src . $post, '\{','\}'); if($blk eq '') { last; } #separate the two parts. $prepart = quotemeta($prepart); $blk =~ m!$prepart(.*)$!is; $postpart = $1; #remove postpart from the ending (make sure to return this optional return val!) $postpart = quotemeta($postpart); $post =~ s!^$postpart!!is; #blk most likely has images. TIME TO REPLACE THEM ALL! # "\"$escaped\""; while($blk =~ m!()!is) { my $timg = quotemeta($1); my $tihash = $2; my $timstring = $texhash{$tihash}; # return "eep: $timg :: $tihash :: $timstring"; $blk =~ s!$timg!$timstring!is; } # return "eep: $pre :: $blk"; $pre .= ' ' . handleLatex($sty . $blk, 'inline="1"') . ' '; $secsearch = ''; last; } } $secsearch = $t2; =fixed #grab the ' in .*? $imgchk = $1; if($imgchk =~ m!\\>!) { } else { #it's part of the last img tag! next; } =cut $pre .= ' ' . handleLatex($sty . $blk,'inline="1"') . ' '; } $pre = $pre . $secsearch; # \\ --> \n $pre =~ s!\\\\!\n!gis; #``()'' --> " .. " $pre =~ s!\`\`(.*?)\'\'!\"$1\"!gis; # $ ... $ # $pre =~ s!\$\s*(.*?)\s*\$!&handleLatex('$' . $1 . '$','inline="1"')!giseo; return ($pre,$post); } #helper function that grabs the right tag (no need for weird divtree) #do not give regex delimiteres that match! sub umbrellaHook { #pass in the process text, and delimiters (in regex) #returns list (before,umbrella,after) (first one it sees) my $txt = $_[0]; my $delim_l = $_[1]; my $delim_r = $_[2]; my $nleft = 0; my $nright = 0; my $umb = ''; my $front; my $before = ''; if($txt =~ m!$delim_l!is) { $nleft++; $before = $`; $umb = $&; $txt = "$'"; # return ($before, $umb,$txt); # my $pl = -1; # my $pr = -1; while($nright < $nleft) { if($txt =~ m!$delim_r!is) { $nright++; } else { #mismatch! $txt = $before . $umb . $txt; $before = ''; $umb = ''; last; } $front = $`; $umb .= $` . $&; $txt = "$'"; #count how many left's are before this right while($front =~ m!$delim_l!is) { $nleft++; $front = "$'"; } # if($pl == $nleft && $pr == $nright) { # die("Delimiter mismatch!"); # } # $pl = $nleft; # $pr = $nright; } } else { } return ($before, $umb, $txt); } # ========================= sub handleLatex { # This function takes a string of math, computes its hash code, and returns a # link to what will be the image representing this math. ### my ( $math_string ) = @_; # do not uncomment, use $_[0], $_[1] instead my $math_string = $_[0]; my $escaped = $_[0]; my $prefs = $_[1]; # remove latex-common HTML entities from within math env $math_string =~ s/&/&/og; $math_string =~ s/</\/og; # set default rendering parameters my %opts = ( 'inline' => 0, 'density' => $default_density, 'gamma' => $default_gamma, 'scale' => $default_scale, 'color' => 'black' ); my %opts2 = TWiki::Func::extractParameters( $prefs ); # map { $opts{$_} = $opts2{$_} } keys %opts2; foreach my $k (keys %opts2) { my $b = $opts2{$k}; # remove leading/trailing whitespace from key names (my $a = $k) =~ s/^\s*|\s*$//; # scrub the inputs, since this gets passed to 'convert' (in # particular, sheild against 'density=166|cat%20/etc/passwd' # type inputs). alpha-numeric OK. slash, space, and brackets # are valid in preamble. need semi-colon in eqn lables! # allow '-' and '_' in eqn labels too. $b =~ m/([\.\\\w\s\:\-\_\{\}]+)/; $b = $1; $opts{$a} = $b; $use_color = 1 if ($a eq 'color'); } if ( ($use_color == 1) and !( $preamble =~ m/ackage\{color/) ) { $preamble = "\\RequirePackage{color}\n".$preamble; } if ( ( $preamble =~ m/package\{color/i) and !($preamble =~ m/definecolor\{Red/) ) { $preamble .= <<'COLORS'; \definecolor{Red}{rgb}{1,0,0} \definecolor{Blue}{rgb}{0,0,1} \definecolor{Yellow}{rgb}{1,1,0} \definecolor{Orange}{rgb}{1,0.4,0} \definecolor{Pink}{rgb}{1,0,1} \definecolor{Purple}{rgb}{0.5,0,0.5} \definecolor{Teal}{rgb}{0,0.5,0.5} \definecolor{Navy}{rgb}{0,0,0.5} \definecolor{Aqua}{rgb}{0,1,1} \definecolor{Lime}{rgb}{0,1,0} \definecolor{Green}{rgb}{0,0.5,0} \definecolor{Olive}{rgb}{0.5,0.5,0} \definecolor{Maroon}{rgb}{0.5,0,0} \definecolor{Brown}{rgb}{0.6,0.4,0.2} \definecolor{Black}{gray}{0} \definecolor{Gray}{gray}{0.5} \definecolor{Silver}{gray}{0.75} \definecolor{White}{gray}{1} COLORS } &TWiki::Func::writeDebug( "- LatexModePlugin::handleLatex( ". $math_string . " :: ". join('; ',map{"$_ => $opts{$_}"}keys(%opts)). " )" ) if $debug; my $txt; if( exists($opts{'label'}) ) { ( $opts{'label'} = "eqn:".$opts{'label'} ) unless ( substr($opts{'label'},0,4) eq 'eqn:' ); } if ($latexout) { if( exists($opts{'label'}) ) { # strip off any 'displaymath' calls $math_string =~ s!\\\[|\\\]!!g; $math_string =~ s!\\(begin|end)\{displaymath\}!!g; if ($math_string =~ m/eqnarray(\*?)/) { # try to handle equation arrays. if ($1 eq '*') { $math_string =~ s/eqnarray\*/eqnarray/g; # leave no numbers ... $math_string =~ s!\\\\!\\nonumber \\\\!g; # except for the last one } # slip the label in.. my $lbl = '\label{'.$opts{'label'}.'}'; $math_string =~ s/(begin\{.*?\})/$1$lbl/; } else { $math_string = "\n\\begin{equation}\n". ' \label{'.$opts{'label'}."}"."\n". " ".$math_string."\n". "\\end{equation}\n"; } } # in Cairo (at least) latex new-lines, '\\', get translated to # spaces, '\', if they appear at the end of the line. # So protect them here... $math_string =~ s!\n! \n!g; $txt = ''.$math_string.''; } else { # compute the MD5 hash of this string, using both the markup text # and the declared options. my $hash_code = md5_hex( $math_string . join('; ', map{"$_=>$opts{$_}"} keys(%opts)) ); #my version. do not delete any entries. put before inlined! $texhash{$hash_code} = $math_string; $math_string = '\fbox{ ' . $math_string . '\vphantom{$\{ \}^\mathrm{th}$} }' if ($opts{'inline'} eq 1); #store the string in a hash table, indexed by the MD5 hash $hashed_math_strings{$hash_code} = $math_string; ### store the declared options for the rendering later... $markup_opts{$hash_code} = \%opts; #remove any quotes in the string, so the alt tag doesn't break $escaped =~ s/\"/"/gso; $escaped =~ s/\n/ /gso; #new line from dev $escaped =~ s!(\u\w\l\w+\u\w)!$1!g; #get rid of dollar signs #get rid of things that interfere with my code! $escaped =~ s/\$//gs; #replace braces with parens $escaped =~ s/\{/\(/gs; $escaped =~ s/\}/\)/gs; #just get rid of slashes $escaped =~ s/\\//gs; my $image_name = "$pubUrlPath/latex$hash_code.$EXT"; # if image currently exists, get its dimensions my $outimg = &TWiki::Func::getPubDir() . "/$web/$topic/"."latex$hash_code.$EXT"; my $str = ""; if (-f $outimg) { my $img = image_info($outimg); $str = sprintf("width=\"%d\" height=\"%d\"", ($opts{'scale'} * $img->{width} ), ($opts{'scale'} * $img->{height}) ); undef($img); } #return a link to an attached image, which we will create later if( ($opts{'inline'} eq 1) or ($opts{'inline'} eq "on") or ($opts{'inline'} eq "true") ) { my $algn = 'middle'; # my $algn = ($escaped =~ m/[\_\}\{]|[yjgpq]/) ? 'middle' : 'bottom' ; $txt = "\"$escaped\""; } elsif( exists($opts{'label'}) ) { $eqn++; $txt = ''. ''."\n". ''. '". "
     '. "\"$escaped\"($eqn)
    \n"; if ( exists( $eqnrefs{ $opts{'label'} } ) ) { $error_catch_all .= "   Error! multiple equation labels '$opts{'label'}' defined.\n". "(Eqns. $eqnrefs{$opts{'label'}} and $eqn)
    \n"; } else { $eqnrefs{ $opts{'label'} } = $eqn; } } else { $txt = "

    \"$escaped\"

    "; } } # end 'if !$latexout'; return($txt); } # ========================= sub endRenderingHandler { # for backwards compatibility with Cairo postRenderingHandler($_[0]); } # ========================= sub postRenderingHandler { # Here we check if we saw any math, try to delete old files, render new math, and clean up ### my ( $text ) = @_; # do not uncomment, use $_[0] instead # my ($a,$b,$c) = umbrellaHook('{ haha trick you \bf{ asdf } asdf} } }','\{','\}'); # $_[0] = "a: $a \n b: $b \n c: $c \n "; # return; my $path; &TWiki::Func::writeDebug( "- LatexModePlugin::postRenderingHandler( $web.$topic )" ) if $debug; #my @revinfo = &TWiki::Func::getRevisionInfo($web, $topic, "", 0); #&TWiki::Func::writeDebug( "- LatexModePlugin: @revinfo" ) if $debug; #check if there was any math in this document return unless scalar( keys( %hashed_math_strings ) ); $_[0] .= "\n


    Twiki LatexModePlugin error messages:
    \n". $error_catch_all if ( length($error_catch_all) > 0 ); #if this is a view script, then we will try to delete old files my $delete_files = ( $script =~ m/^view/ ); my @extfiles; if( $TWiki::Plugins::VERSION >= 1.1 ) { # Dakar interface my ( $meta, undef ) = TWiki::Func::readTopic( $web, $topic ); my %h2 = %{$meta}; @extfiles = @{$h2{FILEATTACHMENT}} if defined($h2{FILEATTACHMENT}); } else { # Cairo interface $path = &TWiki::Func::getPubDir() . "/$web/$topic"; opendir(D,$path); @extfiles = grep(/\.$EXT$/,readdir(D)); closedir(D); } &TWiki::Func::writeDebug( "Scanning file attachments" ) if $debug; foreach my $a ( @extfiles ) { my $fn = ( $TWiki::Plugins::VERSION >= 1.1 ) ? $a->{name} : $a; # print STDERR "\n----\n"; # print STDERR map {"$_ -> $h{$_}\n"} keys %h; # was the image likely generated by this plugin? if( $fn =~ m/^latex[0-9a-f]+\.$EXT$/ ) { my $hash_code = substr( $fn, 5, $HASH_CODE_LENGTH ); #is the image still used in the document? if( exists( $hashed_math_strings{$hash_code} ) ) { #if the image is already there, we don't need to re-render delete( $hashed_math_strings{$hash_code} ); next; } if( $delete_files ) { #delete the old image &TWiki::Func::writeDebug( "Deleting old image that I think belongs to me: $fn" ) if $debug; if ( $fn =~ /^([-\@\w.]+)$/ ) { # untaint filename $fn = $1; if ( $TWiki::Plugins::VERSION >= 1.1 ) { # Dakar interface TWiki::Func::moveAttachment( $web, $topic, $fn, $TWiki::cfg{TrashWebName}, 'TrashAttachment', $fn ); } else { # Cairo interface unlink( $path.$pathSep.$fn ); } } } } } #check if there are any new images to render return unless scalar( keys( %hashed_math_strings ) ); # create a temporary working directory my $LATEXWDIR = File::Temp::tempdir(); &TWiki::Func::writeDebug( "LatexModePlugin working directory: $LATEXWDIR" ) if $debug; ### create the temporary Latex Working Directory... #does the topic's attachment directory exist? if( -e $LATEXWDIR ) { #if it's not really a directory, we can't do anything return unless ( -d $LATEXWDIR ); # FIXME: this section should never be called, but should # report an error in the event that it does &TWiki::Func::writeDebug( "Directory already exists." ) if $debug; } else { #create the directory if it didn't exist return unless mkdir( $LATEXWDIR ); &TWiki::Func::writeDebug( " Directory $LATEXWDIR does not exist" ) if $debug; } # move into the temprorary working directory # use Cwd 'cwd'; # (my $saveddir = cwd) =~ s/^([-\@\w.]+)$/$1/; # $saveddir now untainted my $LATEXLOG = File::Temp::tempnam( $LATEXWDIR, 'latexlog' ); do { $_[0] .= "
    unable to access latex working directory."; return; } unless chdir( $LATEXWDIR ); system("echo \"$LATEXWDIR\n^O\n\" > $LATEXLOG"); my $image_number = 0; # initialize the image count #this hash table maps the digest strings to the output filenames my %hash_code_mapping = (); #create the intermediate latex file do { $_[0] .= "
    can't write $LATEXFILENAME: $!\n"; return; } unless open( MATHOUT, ">$LATEXFILENAME" ); print MATHOUT "\\documentclass{article}\n".$preamble."\n\\begin{document}\n\\pagestyle{empty}\n"; while( (my $key, my $value) = each( %hashed_math_strings ) ) { # restore the declared rendering options my %opts = %{$markup_opts{$key}}; &TWiki::Func::writeDebug( "LatexModePlugin: ". $value . " :: " . join('; ', map{"$_=>$opts{$_}"} keys(%opts)) ) if ($debug); print MATHOUT "\\clearpage\n"; print MATHOUT "% $LATEXBASENAME.$EXT.$image_number --> $key \n"; print MATHOUT '\textcolor{'.$opts{'color'}.'}{' unless ($opts{'color'} eq 'black'); print MATHOUT " $value "; print MATHOUT '}' unless ($opts{'color'} eq 'black'); $hash_code_mapping{$key} = $image_number + 1; $image_number++; } print MATHOUT "\\clearpage\n(end)\\end{document}\n"; close( MATHOUT ); # generate the output images by running latex-dvips-convert on the file system("$PATHTOLATEX $LATEXFILENAME >> $LATEXLOG 2>&1"); ### report errors on 'preview' and 'save' if ( ( $script eq 'preview' ) || ( $script eq 'save' ) ) { my $resp = `$GREP -A 3 -i "!" $LATEXLOG`; $_[0] .= "\n
    Latex rendering error messages:
    $resp
    \n" if ( length($resp) > 0 ); } if ( -f $LATEXBASENAME.".dvi" ) { #generate image files based on the hash code while( (my $key, my $value) = each( %hash_code_mapping ) ) { # restore (again) the rendering options my %opts = %{$markup_opts{$key}}; # calculate point-to-pixel mapping (1pt/72dpi*density) # == 1.61 for density=116 my $ptsz = ($opts{'density'}/72); my $num = $hash_code_mapping{$key}; system("$PATHTODVIPS -E -pp $num -o $LATEXBASENAME.$num.eps $LATEXBASENAME.dvi >> $LATEXLOG 2>&1 "); my $outimg = "latex$key.$EXT"; my $cmd = "-density $opts{'density'} $LATEXBASENAME.$num.eps -antialias -trim "; $cmd .= "-shave ".round(2*$ptsz)."x".round(2*$ptsz)." " if ($markup_opts{$key}{'inline'} ne 0); $cmd .= "-gamma $opts{'gamma'} -transparent white $outimg"; system("echo \"$PATHTOCONVERT $cmd\" >> $LATEXLOG"); system("$PATHTOCONVERT $cmd"); if (-f $outimg) { my $img = image_info($outimg); if ($markup_opts{$key}{'inline'} ne 0) { my $tmpfile = File::Temp::tempnam( $LATEXWDIR, 'tmp' ).".$EXT"; system("$PATHTOCONVERT $outimg -trim $tmpfile"); my $img2 = image_info($tmpfile); my ($nw,$nh) = ( $img2->{width}+4, $img->{height} ); $nw = $1 if ($nw =~ m/(\d+)/); # untaint $nh = $1 if ($nh =~ m/(\d+)/); # untaint my ($sh,$sh2) = ( ( $img->{width} - $nw )/2 , round(2*$ptsz) ); $sh = $1 if ($sh =~ m/(\d+)/); # untaint $sh2 = $1 if ($sh2 =~ m/(\d+)/); # untaint $sh += 5; $sh2 += 2; my $cmd = " -crop ".$nw."x".$nh."+$sh+$sh2 $outimg"; # my $cmd = " -shave ".$maxshave.'x'.$maxshave." $outimg"; copy($outimg,$tmpfile); system("echo \"$PATHTOCONVERT $tmpfile $cmd\" >> $LATEXLOG"); system("$PATHTOCONVERT $tmpfile $cmd"); unlink("$tmpfile") unless ($debug); # refresh the image info $img = image_info($outimg); ## Another strategy: trim gives better horizontal ## results but is too aggressive vertically. ## * convert ps --> 1.png (with a border) ## * shave 1.png by border size ## * copy 1.png --> 2.png ## * trim 2.png ## * extract off image and page size using identify ## (this gives crop coordinates). ## * crop 1.png, using width-coordinates from ## trim and hieght coordinates from shave ### EXAMPLE: # /usr/X11R6/bin/convert -density 116 twiki_math.4.ps -antialias -trim -gamma 0.6 -transparent white t1.png # cp t1.png t2.png # mogrify -shave 2x2 t2.png # identify t2.png # "t2.png PNG 35x24+2+2 PseudoClass 256c 8-bit 365.0 0.000u 0:01" # mogrify -trim t2.png # identify t2.png # "tmp.png PNG 11x11+8+6 PseudoClass 256c 8-bit 306.0 0.000u 0:01" # mogrify -crop 11x24+10+3 t1.png # } my $str = sprintf("width=\"%d.0\" height=\"%d.0\"", ($opts{'scale'} * $img->{width}), ($opts{'scale'} * $img->{height}) ); $_[0] =~ s/($outimg\")/$1 $str/; if( $TWiki::Plugins::VERSION >= 1.1 ) { # Dakar interface TWiki::Func::saveAttachment( $web, $topic, $outimg, { file => $outimg, comment => '', hide => 1 } ); unlink($outimg) unless $debug; # delete working copy } else { # Cairo interface mkdir( $path.$pathSep )unless (-e $path.$pathSep); # $outimg = $LATEXWDIR . '/' . $outimg; move($outimg,$path.$pathSep.$outimg) or $_[0] .= "
    LatexModePlugin error: Move of $outimg failed: $!"; } undef($img); } } } else { $_[0] .= "
    Latex rendering error!! DVI file was not created.
    "; } ##debug # $_[0] .= "
    $LATEXWDIR" . '/' . "$LATEXFILENAME
    "; # return; #clean up the intermediate files unless ($debug) { opendir(D,$LATEXWDIR); my @files = grep(/$LATEXBASENAME/,readdir(D)); close(D); foreach my $fn ( @files ) { #again, we need to untaint the globbed filenames # next if ($fn =~ /index/); if( $fn =~ /^([-\@\w.]+)$/ ) { $fn = $1; # $fn now untainted unlink( "$fn" ); } else { &TWiki::Func::writeDebug( "Bizzare error. match of \$fn failed? $fn" ) if $debug; } } } #clear the hash table of math strings %hashed_math_strings = (); %markup_opts = (); &TWiki::Func::writeDebug( "Math strings reset, done." ) if $debug; # remove the log file unlink($LATEXLOG) unless ($debug); # remove the temporary working directory rmdir($LATEXWDIR); $LATEXWDIR = undef; # move back to the previous directory. # chdir($saveddir) if ( $saveddir ); } sub round { my ($i) = @_; # my $a = ( ($i - int($i)) > 0.5 ) ? int($i) : int($i) + 1; my $a = int($i); $a = $a + 1 if ( ($i - int($i)) > 0.5 ); return($a); } # ========================= 1; __DATA__