diff options
author | Karl Berry <karl@freefriends.org> | 2008-11-20 16:56:11 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2008-11-20 16:56:11 +0000 |
commit | c0c091e5b6a1e5cc7816178df4d725290b8364ab (patch) | |
tree | 206ea9c94063a415d517e5f5cd115615e0f78484 /Master/texmf-dist/scripts/texcount | |
parent | 2ef1581f8d9e92ff91fb3bf3907af849c01c86bc (diff) |
texcount 2.1 (19nov08)
git-svn-id: svn://tug.org/texlive/trunk@11374 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/texcount')
-rwxr-xr-x | Master/texmf-dist/scripts/texcount/TeXcount.pl | 1026 |
1 files changed, 723 insertions, 303 deletions
diff --git a/Master/texmf-dist/scripts/texcount/TeXcount.pl b/Master/texmf-dist/scripts/texcount/TeXcount.pl index 1ed70fc8a56..8b68e4984c0 100755 --- a/Master/texmf-dist/scripts/texcount/TeXcount.pl +++ b/Master/texmf-dist/scripts/texcount/TeXcount.pl @@ -2,15 +2,33 @@ use strict; use warnings; use Term::ANSIColor; -#--- --- ---# use POSIX qw(locale_h); use locale; setlocale(LC_CTYPE,"no_NO"); -my $versionnumber="2.0"; -my $versiondate="2008 Feb 10"; +my $versionnumber="2.1"; +my $versiondate="2008 Nov 02"; -#--- --- ---# +###### Set CMD specific settings and variables + +# Options and states +my $verbose=0; +my $showcodes=1; +my $showstates=0; +my $showsubcounts=0; +my $htmlstyle=0; +my $includeTeX=0; +my $briefsum=0; +my $totalflag=0; +my @sumweights; +my $globalworkdir=""; + +# Global variables +my $blankline=0; +my @filelist; +my $workdir; + +###### Set global settings and variables ### Macros for headers # Macros that identify headers: i.e. following token or @@ -109,10 +127,10 @@ my %TeXgroup=('document'=>1,'letter'=>1,'titlepage'=>0, my %TeXfloatinc=('\caption'=>[3]); ### Macros for including tex files -# Allows \macro{file} or \macro file. If the value is 1, the filetype .tex will -# be added if file is without a filetype; if the value is 0, this will not be -# done. -my %TeXfileinclude=('\input'=>0,'\include'=>1); +# Allows \macro{file} or \macro file. If the value is 0, the filename will +# be used as is; if it is 1, the filetype .tex will be added if the +# filename is without filetype; if it is 2, the filetype .tex will be added. +my %TeXfileinclude=('\input'=>1,'\include'=>2); ### Count labels # Labels used to describe the counts @@ -120,113 +138,284 @@ my @countlabel=('Files','Words in text','Words in headers', 'Words in float captions','Number of headers','Number of floats', 'Number of math inlines','Number of math displayed'); -# Styles +### Break points +# Definition of macros that define break points that start a new subcount. +# The values given are used as labels. +my %BreakPointsOptions; +$BreakPointsOptions{'none'}={}; +$BreakPointsOptions{'part'}={%{$BreakPointsOptions{'none'}},'\part'=>'Part'}; +$BreakPointsOptions{'chapter'}={%{$BreakPointsOptions{'part'}},'\chapter'=>'Chapter'}; +$BreakPointsOptions{'section'}={%{$BreakPointsOptions{'chapter'}},'\section'=>'Section'}; +$BreakPointsOptions{'subsection'}={%{$BreakPointsOptions{'section'}},'\subsection'=>'Subsection'}; +$BreakPointsOptions{'default'}=$BreakPointsOptions{'subsection'}; +my %BreakPoints=%{$BreakPointsOptions{'none'}}; + +### Print styles +# Definition of different print styles: maps of class labels +# to ANSI codes. Class labels are as used by HTML styles. my @STYLES=(); my %STYLE; $STYLES[0]={'error'=>'bold red'}; $STYLES[1]={%{$STYLES[0]}, 'word1'=>'blue','word2'=>'bold blue','word3'=>'blue', 'grouping'=>'red','document'=>'red','mathgroup'=>'magenta', - 'state'=>'cyan underline'}; + 'state'=>'cyan underline','sumcount'=>'yellow'}; $STYLES[2]={%{$STYLES[1]}, - 'word0'=>'yellow','word-1'=>'yellow', 'command'=>'green','exclgroup'=>'yellow','exclmath'=>'yellow', 'ignore'=>'cyan'}; $STYLES[3]={%{$STYLES[2]}, - 'tc'=>'bold yellow','comment'=>'yellow','option'=>'yellow','fileinclude'=>'bold green'}; + 'tc'=>'bold yellow','comment'=>'yellow','option'=>'yellow', + 'fileinclude'=>'bold green'}; $STYLES[4]={%{$STYLES[3]}}; -# Options and states -my $verbose=0; -my $showstates=0; -my $htmlstyle=0; -my $blankline=0; -my $includeTeX=0; -my $briefsum=0; +### Word regexp pattern list +# List of regexp patterns that should be analysed as words. +my $specialchars='\\\\(ae|AE|o|O|aa|AA)'; +my $modifiedchars='\\\\[\'\"\`\~\^\=](\w|\{\w\})'; +my @WordPatterns=('(\w+\.)+\w+\.?','\w+([\-\']\w+)*'); +my @WordPatternsRelaxed=('([\w\-\']|'.$modifiedchars.'|'.$specialchars.'(\{\})?|\{'.$specialchars.'\})+'); -#--- --- ---# +### Macro option regexp list +# List of regexp patterns to be gobbled as macro option in and after +# a macro. +my @MacroOptionPatterns=('\[(\w|[,\-\s\~\.\:\;\+\?\*\_\=])*\]'); +my @MacroOptionPatternsRelaxed=('\[[^\[\]\n]*\]'); -## SHORT HELP AND INFO -if (!defined @ARGV) { - print_version(); - print_syntax(); - print_reference(); - exit; -} elsif ($ARGV[0]=~/^(\-(h|\?|\-?help)|\/\?)$/) { - print_help(); - exit; -} elsif ($ARGV[0]=~/^\-?\-version$/) { - print_version(); - exit; -} elsif ($ARGV[0]=~/^\-?\-license$/) { - print_license(); - exit; -} - -# PROCESS ARGUMENTS AND FILES -my $totalcount=new_count(); -my @toplevelfiles; -my @filelist; -foreach my $arg (@ARGV) { - if ($arg eq '-noinc') {$includeTeX=0; next;} - if ($arg eq '-inc') {$includeTeX=1; next;} - if ($arg eq '-brief') {$briefsum=1; next;} - if ($arg eq '-v0') {$verbose=0; next;} - if ($arg eq '-v1') {$verbose=1; next;} - if ($arg eq '-vv' || $arg eq '-v2') {$verbose=2; next;} - if ($arg eq '-vvv' || $arg eq '-v3' || $arg eq '-v') {$verbose=3; next;} - if ($arg eq '-vvvv' || $arg eq '-v4') {$verbose=3; $showstates=1; next;} - if ($arg =~ /^\-showstate/ ){$showstates=1; next;} - if ($arg =~ /^\-html/ ) { - $ENV{'ANSI_COLORS_DISABLED'} = 1; - $htmlstyle = $arg eq '-htmlcore' ? 1 : 2; - if ($htmlstyle>1) {html_head();} - next; - } - if ($arg=~/^\-(nocol|nc$)/) { - $ENV{'ANSI_COLORS_DISABLED'} = 1; - next; - } - if ($arg=~/^\-/) { - print 'Invalid opton '.$arg."\n"; +###### Main script + +################################################### + +MAIN(@ARGV); + +################################################### + + +######### +######### Main routines +######### + +# MAIN ROUTINE: Handle arguments, then parse files +sub MAIN { + my @args=@_; + my @toplevelfiles=Parse_Arguments(@args); + Apply_Options(); + if (scalar(@toplevelfiles)==0) { + if ($showcodes>1) {print_help_style();} + else {print_error("No files specified.","p","error");} + } else { + conditional_print_help_style(); + my $totalcount=parse_file_list(@toplevelfiles); + conditional_print_total($totalcount); + } + Close_Output(); +} + +# Checks arguments, exits on exit condition +sub Check_Arguments { + my @args=@_; + if (!@args) { + print_version(); print_syntax(); + print_reference(); + exit; + } elsif ($args[0]=~/^(\-?\-(h|\?|help)|\/(\?|h))$/) { + print_help(); + exit; + } elsif ($args[0]=~/^\-?\-(ver|version)$/) { + print_version(); exit; + } elsif ($args[0]=~/^\-?\-(lic|license)$/) { + print_license(); + exit; + } + return 1; +} + +# Parses arguments, sets options (global) and returns file list +sub Parse_Arguments { + my @args=@_; + Check_Arguments(@args); + my @files; + foreach my $arg (@ARGV) { + if (Parse_Option($arg)) {next;} + if ($arg=~/^\-/) { + print 'Invalid opton '.$arg."\n"; + print_syntax(); + exit; + } + $arg=~s/\\/\//g; + push @files,$arg; + } + return @files; +} + +# Parse individual option parameters +sub Parse_Option { + my $arg=shift @_; + return parse_options_parsing($arg) + || parse_options_sums($arg) + || parse_options_output($arg) + || parse_options_format($arg) + ; +} + +sub parse_options_parsing { + my $arg=shift @_; + if ($arg eq '-inc') {$includeTeX=1;} + elsif ($arg eq '-noinc') {$includeTeX=0;} + elsif ($arg eq '-dir') {$globalworkdir=undef;} + elsif ($arg=~/^-dir=(.*)$/) {$globalworkdir=$1;} + elsif ($arg eq '-relaxed') { + @MacroOptionPatterns=@MacroOptionPatternsRelaxed; + @WordPatterns=@WordPatternsRelaxed; + } + else {return 0;} + return 1; +} + +sub parse_options_sums { + my $arg=shift @_; + if ($arg=~/^-sum(=(.+))?$/) {option_sum($2);} + elsif ($arg=~/^-(sub|subcounts?)(=(.+))?$/) {option_subcount($3);} + else {return 0;} + return 1; +} + +sub option_subcount { + my $arg=shift @_; + $showsubcounts=1; + if (!defined $arg) { + %BreakPoints=%{$BreakPointsOptions{'default'}}; + } elsif (my $option=$BreakPointsOptions{$arg}) { + %BreakPoints=%{$option}; + } else { + print STDERR "Warning: Option value ".$arg." not valid, using default instead.\n"; + %BreakPoints=%{$BreakPointsOptions{'default'}}; } - $arg=~s/\\/\//g; - push @toplevelfiles,$arg; } -%STYLE=%{$STYLES[$verbose]}; -for my $file (<@toplevelfiles>) { + +sub option_sum { + my $arg=shift @_; + if (!defined $arg) { + @sumweights=(1,1,1,0,0,1,1); + } elsif ($arg=~/^(\d+(,\d+){0,6})$/) { + @sumweights=split(',',$1); + } else { + print STDERR "Warning: Option value ".$arg." not valid, ignoring option.\n"; + } +} + +sub parse_options_format { + my $arg=shift @_; + if ($arg eq '-brief') {$briefsum=1; return 1;} + elsif ($arg eq '-total') {$totalflag=1; return 1;} + elsif ($arg eq '-1') {$briefsum=1;$totalflag=1;} + elsif ($arg eq "-html" ) {option_no_colours();$htmlstyle = 2;} + elsif ($arg eq "-htmlcore" ) {option_no_colours();$htmlstyle = 1;} + elsif ($arg=~/^\-(nocol|nc$)/) {option_no_colours();} + elsif ($arg eq '-codes') { + $showcodes=2; + if ($verbose==0) {$verbose=3;} + } + elsif ($arg eq '-nocodes') {$showcodes=0;} + else {return 0;} + return 1; +} + +sub parse_options_output { + my $arg=shift @_; + if ($arg eq "-v0") {$verbose=0;} + elsif ($arg eq "-v1") {$verbose=1;} + elsif ($arg eq '-vv' || $arg eq '-v2') {$verbose=2;} + elsif ($arg eq '-vvv' || $arg eq '-v3' || $arg eq '-v') {$verbose=3;} + elsif ($arg eq '-vvvv' || $arg eq '-v4') {$verbose=3; $showstates=1;} + elsif ($arg =~ /^\-showstates?$/ ){$showstates=1;} + else {return 0;} + return 1; +} + +# Parse file list and return total count +sub parse_file_list { + my @filelist=@_; + my $listtotalcount=new_count("TOTAL COUNT"); + for my $file (<@filelist>) { + my $filetotalcount=parse_file($file); + add_count($listtotalcount,$filetotalcount); + } + return $listtotalcount; +} + +# Parse file and included files, and return total count +sub parse_file { + my $file=shift @_; + $workdir=$globalworkdir; + if (!defined $workdir) { + $workdir=$file; + $workdir =~ s/^((.*[\\\/])?)[^\\\/]+$/$1/; + } @filelist=($file); + if ($htmlstyle) {print "\n<div class='filegroup'>\n";} + my $filetotalcount=new_count("SUM COUNT FOR ".$file); foreach my $f (@filelist) { my $tex=TeXfile($f); my $fpath=$f; - $fpath=~s/^(.*[\\\/])[^\\\/]+/$1/; + $fpath=~s/^((.*[\\\/])?)[^\\\/]+$/$1/; if (!defined $tex) { - print "File not found or not readable: ".$f."\n"; + #print_error("File not found or not readable: ".$f."\n"); + formatprint("File not found or not readable: ".$f."\n","p","error"); } else { parse($tex); - print "\n"; - print_count($tex->{'count'},'FILE: '.$f); - print "\n"; - add_count($totalcount,$tex->{'count'}); + my $filecount=add_subcount($tex); + if (!$totalflag) { + print_count($filecount); + print "\n"; + } + add_count($filetotalcount,$filecount); } } + if ($htmlstyle) {print "</div>\n\n";} + return $filetotalcount; } -if (${$totalcount}[0]>1) { - print "\n"; - formatprint('Total','h1'); - print_count($totalcount,'SUM'); + +###### +###### Subroutines +###### + +###### Option handling + +# Apply options to set values +sub Apply_Options { + %STYLE=%{$STYLES[$verbose]}; + if ($htmlstyle>1) {html_head();} } -if ($htmlstyle>1) { - html_tail(); + +# Close the output, e.g. adding HTML tail +sub Close_Output { + if ($htmlstyle>1) { + html_tail(); + } } +sub option_no_colours { + $ENV{'ANSI_COLORS_DISABLED'} = 1; +} +# Print count (total) if conditions are met +sub conditional_print_total { + my $sumcount=shift @_; + if ($totalflag || get_count($sumcount,0)>1) { + if ($totalflag && $briefsum && @sumweights) { + print total_count($sumcount); + } else { + if ($htmlstyle) { + formatprint("Total word count",'h2'); + } + print_count($sumcount); + } + } +} -######### -######### Subroutines -######### +###### TeX File handle sub TeXfile { my $filename=shift @_; @@ -235,7 +424,7 @@ sub TeXfile { } sub TeXcode { - my ($texcode,$filename)=@_; + my ($texcode,$filename,$title)=@_; my %TeX=(); $TeX{'filename'}=$filename; if (!defined $filename) { @@ -245,204 +434,39 @@ sub TeXcode { } else { $TeX{'filepath'}=''; } + if (defined $title) {} + elsif (defined $filename) {$title="FILE: ".$filename;} + else {$title="Word count";} $TeX{'line'}=$texcode; $TeX{'next'}=undef; $TeX{'type'}=undef; $TeX{'style'}=undef; $TeX{'printstate'}=undef; $TeX{'eof'}=0; - $TeX{'count'}=new_count(); - $TeX{'count'}[0]++; + my $countsum=new_count($title); + $TeX{'countsum'}=$countsum; + my $count=new_count("_top_"); + $TeX{'count'}=$count; + inc_count(\%TeX,0); + my @countlist=(); + $TeX{'countlist'}=\@countlist; + $countsum->{'subcounts'}=\@countlist; return \%TeX; } -sub set_style { - my ($tex,$style)=@_; - if (!(($tex->{'style'}) && ($tex->{'style'} eq '-'))) {$tex->{'style'}=$style;} -} - -sub flush_style { - my ($tex,$style)=@_; - set_style($tex,$style); - flush_next($tex); -} - -sub line_return { - my $blank=shift @_; - if ($blank>$blankline) { - if ($htmlstyle) {print "<br>\n";} else {print "\n";} - $blankline++; - } -} - -sub print_style { - my ($text,$style,$state)=@_; - (($verbose>=0) && (defined $text) && (defined $style)) || return; - my $colour; - ($colour=$STYLE{$style}) || return; - if (($colour) && !($colour eq '-')) { - if ($htmlstyle) { - print "<span class='".$style."'>".$text."</span>"; - } else { - print Term::ANSIColor::colored($text,$colour); - } - if ($state) { - print_style($state,'state'); - } - $blankline=-1; - } -} - -sub print_error { - my $text=shift @_; - line_return(1); - print_style("### ".$text." ###",'error'); - line_return(1); -} - -sub formatprint { - my ($text,$tag,$class)=@_; - my $break=($text=~s/\n$//); - if ($htmlstyle && defined $tag) { - print '<'.$tag; - if ($class) {print " class='".$class."'";} - print '>'.$text.'</'.$tag.'>'; - } else { - print $text; - } - if ($break) {print "\n";} -} - sub read_file { my $filename=shift @_; open(FH,"<".$filename."") || return undef; if ($verbose) { - #line_return(1); formatprint("File: ".$filename."\n",'h2'); $blankline=0; } - my @text=(); - while (my $line=<FH>) { - push @text,$line; - } + my @text=<FH>; close(FH); return join('',@text); } -sub next_token { - my $tex=shift @_; - my ($next,$type); - if (defined $tex->{'next'}) {print_style($tex->{'next'}.' ',$tex->{'style'});} - $tex->{'style'}=undef; - while (defined ($next=get_next_token($tex))) { - $type=$tex->{'type'}; - if ($type==0) { - print_style($next,'comment'); - } elsif ($type==9) { - if ($verbose) {line_return(1);} - } else { - return $next; - } - } - return $next; -} - -sub flush_next { - my $tex=shift @_; - if (defined $tex->{'next'}) { - print_style($tex->{'next'}.' ',$tex->{'style'},$tex->{'printstate'}); - } - $tex->{'printstate'}=undef; - $tex->{'style'}='-'; -} - -sub get_next_token { - # Token (or token group) category: - # 0: comment - # 1: word (or other forms of text or text components) - # 2: symbol (not word, e.g. punctuation) - # 3: macro - # 4: curly braces {} - # 5: brackets [] - # 6: maths - # 999: end of line or blank line - # 666: TeXcount instruction (%TC:instruction) - my $tex=shift @_; - my $next; - (defined ($next=get_token($tex,'\%+TC:[^\n]*',666))) && return $next; - (defined ($next=get_token($tex,'\%[^\n]*',0))) && return $next; - (defined ($next=get_token($tex,'\n',9))) && return $next; - (defined ($next=get_token($tex,'\\\\[\{\}]',2))) && return $next; - (defined ($next=get_token($tex,'(\w+\.)+\w+\.?',1))) && return $next; - (defined ($next=get_token($tex,'\w+([\-\']\w+)*',1))) && return $next; - (defined ($next=get_token($tex,'[\"\'\`:\.,\(\)\[\]!\+\-\*=/\^\_\@\<\>\~\#\&]',2))) && return $next; - (defined ($next=get_token($tex,'\\\\([a-zA-Z_]+|[^a-zA-Z_])',3))) && return $next; - (defined ($next=get_token($tex,'[\{\}]',4))) && return $next; - (defined ($next=get_token($tex,'[\[\]]',5))) && return $next; - (defined ($next=get_token($tex,'\$\$',6))) && return $next; - (defined ($next=get_token($tex,'\$',6))) && return $next; - (defined ($next=get_token($tex,'.',999))) && return $next; - (defined ($next=get_token($tex,'[^\s]+',999))) && return $next; - $tex->{'eof'}=1; - return undef; -} - -sub get_token { - my ($tex,$regexp,$type)=@_; - if (!defined $regexp) {print_error("ERROR in get_token: undefined regex.");} - if (!defined $tex->{'line'}) {print_error("ERROR in get_token: undefined tex-line. ".$tex->{'next'});} - if ( $tex->{'line'} =~ s/^($regexp)[ \t\r\f]*// ) { - $tex->{'next'}=$1; - $tex->{'type'}=$type; - return $1; - } - return undef; -} - -sub new_count { - my @count=(0,0,0,0,0,0,0,0); - # files, text words, header words, float words, - # headers, floats, math-inline, math-display; - return \@count; -} - -sub count_word { - my ($count,$type,$word,$style,$verb)=@_; - ($word) || ($word=""); - ($style) || ($style=0); - ($verb) || ($verb=0); - if ($type>0) {${$count}[$type]++;} - if ($verb >= $verbose) { - } -} - -sub print_count { - my ($count,$header)=@_; - if ($briefsum) { - if ($htmlstyle) {print "<p class='count'>\n";} - print ${$count}[1]."+".${$count}[2]."+".${$count}[3]. - " (".${$count}[4]."/".${$count}[5]."/".${$count}[6]."/".${$count}[7].") ". - $header; - if ($htmlstyle) {print "</p>\n";} - return; - } - if ($htmlstyle) {print "<dl class='count'>\n";} - if (defined $header) { - formatprint($header."\n",'dt','header'); - } - for (my $i=1;$i<8;$i++) { - formatprint($countlabel[$i].': ','dt'); - formatprint(${$count}[$i]."\n",'dd'); - } - if ($htmlstyle) {print "</dl>\n";} -} - -sub add_count { - my ($a,$b)=@_; - for (my $i=0;$i<8;$i++) { - ${$a}[$i]+=${$b}[$i]; - } -} +###### Parsing routines sub parse { my ($tex)=@_; @@ -471,7 +495,6 @@ sub parse_unit { print_error("CRITICAL ERROR: Invalid parser status!"); exit; } - my $count=$tex->{'count'}; my $substat; if ($showstates) { if (defined $end) { @@ -509,7 +532,7 @@ sub parse_unit { } elsif ($tex->{'type'}==1) { # word if ($status>0) { - ${$count}[$status]++; + inc_count($tex,$status); set_style($tex,'word'.$status); } } elsif ($next eq '{') { @@ -519,25 +542,31 @@ sub parse_unit { set_style($tex,'ignore'); } elsif ($tex->{'type'}==3) { # macro call + if (my $label=$BreakPoints{$next}) { + if ($tex->{'line'}=~ /^[*]?(\s*\[.*?\])*\s*\{(.+?)\}/ ) { + $label=$label.': '.$2; + } + add_subcount($tex,$label); + } set_style($tex,'command'); if ($next eq '\begin' && $status!=-2) { - parse_begin_end($tex,$count,$status); + parse_begin_end($tex,$status); } elsif (($status==-1) && ($substat=$TeXfloatinc{$next})) { # text included from float gobble_macro_parms($tex,$substat); } elsif ($status==-9 && defined ($substat=$TeXpreamble{$next})) { # parse preamble include macros - if (defined $TeXheader{$next}) {${$count}[4]++;} + if (defined $TeXheader{$next}) {inc_count($tex,4);} gobble_macro_parms($tex,$substat,1); } elsif ($status<0) { # ignore gobble_option($tex); } elsif ($next eq '\(') { # math inline - parse_math($tex,$count,$status,6,'\)'); + parse_math($tex,$status,6,'\)'); } elsif ($next eq '\[') { # math display - parse_math($tex,$count,$status,7,'\]'); + parse_math($tex,$status,7,'\]'); } elsif ($next eq '\def') { # ignore \def... $tex->{'line'} =~ s/^([^\{]*)\{/\{/; @@ -546,14 +575,14 @@ sub parse_unit { parse_unit($tex,-2); } elsif (defined (my $addsuffix=$TeXfileinclude{$next})) { # include file: queue up for parsing - parse_include_file($tex,$count,$status,$addsuffix); + parse_include_file($tex,$status,$addsuffix); } elsif (defined ($substat=$TeXmacro{$next})) { # macro: exclude options - if (defined $TeXheader{$next}) {${$count}[4]++;} + if (defined $TeXheader{$next}) {inc_count($tex,4);} gobble_macro_parms($tex,$substat,$status); } elsif (defined ($substat=$TeXmacroword{$next})) { # count macro as word (or a given number of words) - ${$count}[$status]+=$substat; + inc_count($tex,$status,$substat); set_style($tex,'word'.$status); } elsif ($next =~ /^\\[^\w\_]/) { } else { @@ -561,10 +590,12 @@ sub parse_unit { } } elsif ($next eq '$') { # math inline - parse_math($tex,$count,$status,6,'$'); + parse_math($tex,$status,6,'$'); } elsif ($next eq '$$') { # math display (unless already in inlined math) - if (! $end eq '$') {parse_math($tex,$count,$status,7,'$$');} + if (!(defined $end && $end eq '$')) { + parse_math($tex,$status,7,'$$'); + } } if (!defined $end) {return;} } @@ -573,21 +604,32 @@ sub parse_unit { sub gobble_option { my $tex=shift @_; flush_next($tex); - if ($tex->{'line'} =~ s/^(\[(\w|[,\-\s\~\.\:\;\+\?\*\_\=])*\])//) { - print_style($1,'option'); - return $1; + foreach my $pattern (@MacroOptionPatterns) { + if ($tex->{'line'}=~s/^($pattern)//) { + print_style($1,'option'); + return $1; + } } return undef; } sub parse_tc { my ($tex)=@_; + my $next=$tex->{'next'}; set_style($tex,'tc'); - if ($tex->{'next'}=~/^\%+TC:\s*(\w+)\s+([\\]*\w+)\s+([^\s\n]+)(\s+([0-9]+))?/) { - my $instr=$1; - my $macro=$2; - my $param=$3; - my $option=$5; + flush_next($tex); + if (!($next=~s/^\%+TC:\s*(\w+)\s*// )) { + print_error('Warning: TC command should have format %TC:instruction [macro] [parameters]'); + return; + }; + my $instr=$1; + if ($instr=~/^(break)$/) { + if ($instr eq 'break') {add_subcount($tex,$next);} + } elsif ($next=~/^([\\]*\w+)\s+([^\s\n]+)(\s+([0-9]+))?/) { + # Format = TC:word macro + my $macro=$1; + my $param=$2; + my $option=$4; if ($param=~/^\[([0-9,]+)\]$/) {$param=[split(',',$1)];} if (($instr eq 'macro') || ($instr eq 'exclude')) {$TeXmacro{$macro}=$param;} elsif ($instr eq 'header') {$TeXheader{$macro}=$param;$TeXmacro{$macro}=$param;} @@ -596,24 +638,34 @@ sub parse_tc { elsif ($instr eq 'group') {$TeXmacro{'begin'.$macro}=$param;$TeXgroup{$macro}=$option;} elsif ($instr eq 'floatinclude') {$TeXfloatinc{$macro}=$param;} elsif ($instr eq 'fileinclude') {$TeXfileinclude{$macro}=$param;} + elsif ($instr eq 'breakmacro') {$BreakPoints{$macro}=$param;} else {print_error("Warning: Unknown TC command: ".$instr);} - } elsif ($tex->{'next'}=~/^\%+TC:\s*(\w+)/) { - my $instr=$1; - print_error("Warning: Unknown TC command: ".$instr); + } elsif ($instr eq 'ignore') { + tc_ignore_input($tex); + } else { + print_error("Warning: Invalid TC command format: ".$instr); } } +sub tc_ignore_input { + my ($tex)=@_; + set_style($tex,'ignore'); + parse_unit($tex,-3,"%TC:endignore"); + set_style($tex,'tc'); + flush_next($tex); +} + sub parse_math { - my ($tex,$count,$status,$substat,$end)=@_; + my ($tex,$status,$substat,$end)=@_; my $localstyle=$status>0 ? 'mathgroup' : 'exclmath'; - if ($status>0) {${$count}[$substat]++;} + if ($status>0) {inc_count($tex,$substat);} set_style($tex,$localstyle); parse_unit($tex,0,$end); set_style($tex,$localstyle); } sub parse_begin_end { - my ($tex,$count,$status)=@_; + my ($tex,$status)=@_; my $localstyle=$status>0 ? 'grouping' : 'exclgroup'; flush_style($tex,$localstyle); gobble_option($tex); @@ -635,11 +687,11 @@ sub parse_begin_end { if ($status<=0 && $status<$substat) {$substat=$status;} if (($status>0) && ($substat==-1)) { # Count float - ${$count}[5]++; + inc_count($tex,5); } if ($status>0 and $substat>3) { # count item, exclude contents - ${$count}[$substat]++; + inc_count($tex,$substat); $substat=0; } parse_unit($tex,$substat,'\end'); @@ -653,7 +705,7 @@ sub parse_begin_end { } sub parse_include_file { - my ($tex,$count,$status,$addsuffix)=@_; + my ($tex,$status,$addsuffix)=@_; $tex->{'line'} =~ s/^\{([^\{\}\s]+)\}// || $tex->{'line'} =~ s/^\s*([^\{\}\%\\\s]+)// || return; @@ -661,8 +713,10 @@ sub parse_include_file { if ($status>0) { print_style($&,'fileinclude'); my $fname=$1; - if ($addsuffix && ($fname=~/^[^\.]+$/)) {$fname.='.tex';} - if ($includeTeX) {push @filelist,$tex->{'filepath'}.$fname;} + if ($addsuffix==2) {$fname.='.tex';} + elsif ($addsuffix==1 && ($fname=~/^[^\.]+$/)) {$fname.='.tex';} + #if ($includeTeX) {push @filelist,$tex->{'filepath'}.$fname;} + if ($includeTeX) {push @filelist,$workdir.$fname;} } else { print_style($&,'ignored'); } @@ -710,7 +764,264 @@ sub new_status { return $substat; } -### HTML context +sub next_token { + my $tex=shift @_; + my ($next,$type); + if (defined $tex->{'next'}) {print_style($tex->{'next'}.' ',$tex->{'style'});} + $tex->{'style'}=undef; + while (defined ($next=get_next_token($tex))) { + $type=$tex->{'type'}; + if ($type==0) { + print_style($next,'comment'); + } elsif ($type==9) { + if ($verbose) {line_return(1,$tex);} + } else { + return $next; + } + } + return $next; +} + + +sub get_next_token { + # Token (or token group) category: + # 0: comment + # 1: word (or other forms of text or text components) + # 2: symbol (not word, e.g. punctuation) + # 3: macro + # 4: curly braces {} + # 5: brackets [] + # 6: maths + # 9: line break in file + # 999: end of line or blank line + # 666: TeXcount instruction (%TC:instruction) + my $tex=shift @_; + my $next; + (defined ($next=get_token($tex,'\%+TC:[^\n]*',666))) && return $next; + (defined ($next=get_token($tex,'\%[^\n]*',0))) && return $next; + (defined ($next=get_token($tex,'\n',9))) && return $next; + (defined ($next=get_token($tex,'\\\\[\{\}]',2))) && return $next; + foreach my $pattern (@WordPatterns) { + (defined ($next=get_token($tex,$pattern,1))) && return $next; + } + (defined ($next=get_token($tex,'[\"\'\`:\.,\(\)\[\]!\+\-\*=/\^\_\@\<\>\~\#\&]',2))) && return $next; + (defined ($next=get_token($tex,'\\\\([a-zA-Z_]+|[^a-zA-Z_])',3))) && return $next; + (defined ($next=get_token($tex,'[\{\}]',4))) && return $next; + (defined ($next=get_token($tex,'[\[\]]',5))) && return $next; + (defined ($next=get_token($tex,'\$\$',6))) && return $next; + (defined ($next=get_token($tex,'\$',6))) && return $next; + (defined ($next=get_token($tex,'.',999))) && return $next; + (defined ($next=get_token($tex,'[^\s]+',999))) && return $next; + $tex->{'eof'}=1; + return undef; +} + +sub get_token { + my ($tex,$regexp,$type)=@_; + if (!defined $regexp) {print_error("ERROR in get_token: undefined regex.");} + if (!defined $tex->{'line'}) {print_error("ERROR in get_token: undefined tex-line. ".$tex->{'next'});} + if ( $tex->{'line'} =~ s/^($regexp)[ \t\r\f]*// ) { + $tex->{'next'}=$1; + $tex->{'type'}=$type; + return $1; + } + return undef; +} + +###### Count handling routines + +sub new_count { + my ($title)=@_; + my @cnt=(0,0,0,0,0,0,0,0); + my %count=('count'=>\@cnt,'title'=>$title); + # files, text words, header words, float words, + # headers, floats, math-inline, math-display; + return \%count; +} + +sub inc_count { + my ($tex,$type,$value)=@_; + my $count=$tex->{'count'}; + if (!defined $value) {$value=1;} + ${$count->{'count'}}[$type]+=$value; +} + +sub get_count { + my ($count,$type)=@_; + return ${$count->{'count'}}[$type]; +} + +sub total_count { + my ($count)=@_; + my $sum=0; + for (my $i=scalar(@sumweights);$i-->0;) { + $sum+=get_count($count,$i+1)*$sumweights[$i]; + } + return $sum; +} + +sub print_count { + my ($count,$header)=@_; + if ($briefsum && @sumweights) { + print_count_total($count,$header); + } elsif ($briefsum) { + if ($htmlstyle) {print "<p class='briefcount'>";} + print_count_brief($count,$header); + if ($htmlstyle) {print "</p>\n";} + } else { + print_count_details($count,$header); + } +} + +sub print_count_with_header { + my ($count,$header)=@_; + if (!defined $header) {$header=$count->{'title'};} + if (!defined $header) {$header="";} + return $count,$header; +} + +sub print_count_total { + my ($count,$header)=print_count_with_header(@_); + if ($htmlstyle) {print "<p class='count'>".$header;} + print total_count($count); + if ($htmlstyle) {print "</p>\n";} + else {print ": ".$header;} +} + +sub print_count_brief { + my ($count,$header)=print_count_with_header(@_); + my $cnt=$count->{'count'}; + print ${$cnt}[1]."+".${$cnt}[2]."+".${$cnt}[3]. + " (".${$cnt}[4]."/".${$cnt}[5]."/".${$cnt}[6]."/".${$cnt}[7].") ". + $header; +} + +sub print_count_details { + my ($count,$header)=print_count_with_header(@_); + if ($htmlstyle) {print "<dl class='count'>\n";} + if (defined $header) { + formatprint($header."\n",'dt','header'); + } + if (get_count($count,0)>1) { + formatprint($countlabel[0].': ','dt'); + formatprint(get_count($count,0)."\n",'dd'); + } + if (@sumweights) { + formatprint('Sum count: ','dt'); + formatprint(total_count($count)."\n",'dd'); + } + for (my $i=1;$i<8;$i++) { + formatprint($countlabel[$i].': ','dt'); + formatprint(get_count($count,$i)."\n",'dd'); + } + my $subcounts=$count->{'subcounts'}; + if ($showsubcounts && defined $subcounts && scalar(@{$subcounts})>1) { + formatprint("Subcounts: text+headers+captions (#headers/#floats/#inlines/#displayed)\n",'dt'); + foreach my $subcount (@{$subcounts}) { + if ($htmlstyle) {print "<dd class='briefcount'>";} + print_count_brief($subcount); + if ($htmlstyle) {print "</dd>";} + print "\n"; + } + } + if ($htmlstyle) {print "</dl>\n";} +} + +sub add_count { + my ($a,$b)=@_; + for (my $i=0;$i<8;$i++) { + ${$a->{'count'}}[$i]+=${$b->{'count'}}[$i]; + } +} + +sub add_subcount { + my ($tex,$title)=@_; + add_count($tex->{'countsum'},$tex->{'count'}); + push @{$tex->{'countlist'}},$tex->{'count'}; + $tex->{'count'}=new_count($title); + return $tex->{'countsum'}; +} + +###### Printing routines + +sub set_style { + my ($tex,$style)=@_; + if (!(($tex->{'style'}) && ($tex->{'style'} eq '-'))) {$tex->{'style'}=$style;} +} + +sub flush_style { + my ($tex,$style)=@_; + set_style($tex,$style); + flush_next($tex); +} + +sub line_return { + my ($blank,$tex)=@_; + if ($blank>$blankline) { + if ((defined $tex) && @sumweights) { + my $num=total_count($tex->{'count'}); + print_style(" [".$num."]","sumcount"); + } + linebreak(); + $blankline++; + } +} + +sub linebreak { + if ($htmlstyle) {print "<br>\n";} else {print "\n";} +} + +sub print_style { + my ($text,$style,$state)=@_; + (($verbose>=0) && (defined $text) && (defined $style)) || return 0; + my $colour; + ($colour=$STYLE{$style}) || return; + if (($colour) && !($colour eq '-')) { + if ($htmlstyle) { + print "<span class='".$style."'>".$text."</span>"; + } else { + print Term::ANSIColor::colored($text,$colour); + } + if ($state) { + print_style($state,'state'); + } + $blankline=-1; + return 1; + } else { + return 0; + } +} + +sub print_error { + my $text=shift @_; + line_return(1); + print_style("### ".$text." ###",'error'); + line_return(1); +} + +sub formatprint { + my ($text,$tag,$class)=@_; + my $break=($text=~s/\n$//); + if ($htmlstyle && defined $tag) { + print '<'.$tag; + if ($class) {print " class='".$class."'";} + print '>'.$text.'</'.$tag.'>'; + } else { + print $text; + } + if ($break) {print "\n";} +} + +sub flush_next { + my $tex=shift @_; + if (defined $tex->{'next'}) { + print_style($tex->{'next'}.' ',$tex->{'style'},$tex->{'printstate'}); + } + $tex->{'printstate'}=undef; + $tex->{'style'}='-'; +} + +###### HTML routines sub html_head { print ' @@ -718,13 +1029,11 @@ sub html_head { <head> <style> <!-- -body {width:100%;padding:5;margin:5 10 5 5;} +body {width:auto;padding:5;margin:5;} .error {font-weight:bold;color:#f00;font-style:italic;} -.parse {font-size: 80%; background: #eef;} .word1,.word2,.word3 {color: #009;} .word2 {font-weight: 700;} .word3 {font-style: italic;} -.word-1 {color: #66c;} .command {color: #c00;} .option {color: #cc0;} .grouping, .document {color: #900; font-weight:bold;} @@ -735,14 +1044,19 @@ body {width:100%;padding:5;margin:5 10 5 5;} .tc {color: #999; font-weight:bold;} .comment {color: #999; font-style: italic;} .state {color: #990; font-size: 70%;} +.sumcount {color: #999; font-size: 80%;} .fileinclude {color: #696; font-weight:bold;} dl.count {background: #cfc; color: 009;} -dl.count dt.header {font-weight: 700; font-style: italic; float: none;} -dl.count dt {clear: both; float: left;} -dl.count dd {font-weight: 700;} +dl.count dt.header {font-weight: bold; font-style: italic; float: none;} +dl.count dt {clear: both; float: left; margin-right: .5em;} +dl.count dd {font-weight: bold;} +dl.count dd.briefcount {font-weight: 700; clear: both; font-size:80%; font-weight:normal; margin-left:8pt;} .warning {color: #c00; font-weight: 700;} -.parse, .count {border: solid 1px #999; margin: 0pt; padding: 4pt;} +.parse, .count, .stylehelp, .filegroup {border: solid 1px #999; margin: 0pt; padding: 4pt;} +.parse {font-size: 80%; background: #eef;} .parse {border-bottom:none;} +.stylehelp {font-size: 80%; background: #ffc; margin-bottom: 8pt;} +.filegroup {background: #efe; margin-bottom: 8pt;} --> </style> </head> @@ -755,8 +1069,7 @@ sub html_tail { print '</body></html>'; } - -### HELP AND INFO +###### Help routines sub print_version { print "TeXcount version ".$versionnumber.", ".$versiondate.'.'; @@ -767,18 +1080,43 @@ sub print_syntax { Syntax: TeXcount.pl [options] files Options: + -relaxed Uses relaxed rules for word and option handling: + i.e. allows more general cases to be counted as + either words or macros. -v Verbose (same as -v3) -v0 Do not present parsing details -v1 Verbose: print parsed words, mark formulae -v2 More verbose: also print ignored text -v3 Even more verbose: include comments and options + -v4 Same as -v3 -showstate -showstate Show internal states (with verbose) -brief Only prints a brief, one line summary of counts + -sum, -sum= Make sum of all word and equation counts. May also + use -sum=#[,#] with up to 7 numbers to indicate how + each of the counts (text words, header words, caption + words, #headers, #floats, #inlined formulae, + #displayed formulae) are summed. The default sum (if + only -sum is used) is the same as -sum=1,1,1,0,0,1,1. + -sub, -sub= Generate subcounts. Option values are none, part, + chapter, section or subsection. Default (-sub) is set + to subsection, whereas unset is none. (Alternative + option name is -subcount.) -nc, -nocol No colours (colours require ANSI) - -inc Include tex files included in the document - -noinc Do not include included tex files (default) -html Output in HTML format -htmlcore Only HTML body contents + -inc Include tex files included in the document + -noinc Do not include included tex files (default) + -total Do not give sums per file, only total sum. + -1 Same as -brief and -total. Ensures there is only one + line of output. If used in conjunction with -sum, the + output will only be the total number. (NB: Character + is the number one, not the letter L.) + -dir, -dir= Specify the working directory using -dir=path. + Remember that the path must end with \ or /. If only + -dir is used, the directory of the parent file is used. + -codes Display output style code overview and explanation. + This is on by default. + -nocodes Do not display output style code overview. -h, -?, --help, /? Help --version Print version number --license License information @@ -821,6 +1159,37 @@ ensure that words in the text has been interpreted as such, whereas mathematical formulae and text/non-text in begin-end groups have been correctly interpreted. +Parsing instructions may be passed to TeXcount using comments +in the LaTeX files on the format + %TC:instruction arguments +where valid instructions for setting parsing rules, typically +set at the start of the document (applies globally), are: + %TC:macro [macro] [param.states] + macro handling rule, no. of and rules for parameters + %TC:macroword [macro] [number] + macro counted as a given number of words + %TC:header [macro] [param.states] + header macro rule, as macro but counts as one header + %TC:breakmacro [macro] [label] + macro causing subcount break point + %TC:group [name] [parsing-state] + begin-end-group handling rule + %TC:floatinclude [macro] [param.states] + as macro, but also counted inside floats + %TC:preambleinclude [macro] [param.states] + as macro, but also counted inside the preamble + %TC:fileinclue [macro] [rule] + file include, add .tex if rule=2, not if rule=0 +The [param.states] is used to indicate the number of parameters +used by the macro and the rules of handling each of these: format +is [#,#,...,#] with one number for each parameter, and main rules +are 0 to ignore and 1 to count as text. Parsing instructions +which may be used anywhere are: + %TC:ignore start block to ignore + %TC:endignore end block to ignore + %TC:break [title] add subcount break point here +See the documentation for more details. + Unix hint: Use \'less -r\' instead of just \'less\' to view output: the \'-r\' option makes less treat text formating codes properly. @@ -830,6 +1199,7 @@ options or the output will be riddled with colour codes. Instead, you can use -html to produce HTML code, write this to file and view with your favourite browser. '; + print_help_style(); print_reference(); } @@ -864,3 +1234,53 @@ The script has LPPL status "maintained" with Einar Andreas Rødland being the current maintainer. '; } + +sub print_help_style { + if ($verbose<=0) {return;} + formatprint("Format/colour codes of verbose output:","h2"); + print "\n\n"; + if ($htmlstyle) {print "<p class='stylehelp'>";} + help_style_line('Text which is counted',"word1","counted as text words"); + help_style_line('Header and title text',"word2","counted as header words"); + help_style_line('Caption text and footnotes',"word3","counted as caption words"); + help_style_line("Ignored text or code","ignore","excluded or ignored"); + help_style_line('\documentclass',"document","document start, beginning of preamble"); + help_style_line('\macro',"command","macro not counted, but parameters may be"); + help_style_line("[Macro options]","option","not counted"); + help_style_line('\begin{group} \end{group}',"grouping","begin/end group"); + help_style_line('\begin{group} \end{group}',"exclgroup","begin/end group in excluded region"); + help_style_line('$ $',"mathgroup","counted as one equation"); + help_style_line('$ $',"exclmath","equation in excluded region"); + help_style_line('% Comments',"comment","not counted"); + help_style_line('%TC:TeXcount instructions',"tc","not counted"); + help_style_line("File to include","fileinclude","not counted but file may be counted later"); + if ($showstates) { + help_style_line('[state]',"state","internal TeXcount state"); + } + if (@sumweights) { + help_style_line('[sumcount]',"sumcount","cumulative sum count"); + } + help_style_line("ERROR","error","TeXcount error message"); + if ($htmlstyle) {print "</p>";} + print "\n\n"; +} + +sub help_style_line { + my ($text,$style,$comment)=@_; + if ($htmlstyle) { + $comment=" .... ".$comment; + } else { + $comment=" .... ".$comment; + } + if (print_style($text,$style)) { + print $comment; + linebreak(); + } +} + +# Print output style codes if conditions are met +sub conditional_print_help_style { + if ($showcodes) {print_help_style();} + return $showcodes; +} + |