#!/usr/bin/perl # Filename: scurvy # Author: David Ljung Madison # See License: http://MarginalHacks.com/License/ # Description: Screenplay/screenwriting tool: txt->script formatter # See: http://screenplay.sourceforge.net/ use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; ################################################## # Usage ################################################## sub fatal { foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR: $msg\n"; } exit(-1); } sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR "\n"; print STDERR "Usage:\t$PROGNAME [-d] \n"; print STDERR "\tFormats a script\n"; print STDERR "\t-d\tSet debug mode\n"; print STDERR "\t-c\tCount headings\n"; print STDERR "\t-C\tShow \"Continued\" page breaks\n"; print STDERR "\t-i\tAdd initial indent\n"; print STDERR "\t-n\tShow page/line numbers\n"; print STDERR "\n"; exit -1; } sub parse_args { my %opt; # Defaults $opt{per_page} = 53; while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-c$/) { $opt{count_head}=1; next; } if ($arg =~ /^-C$/) { $opt{page_breaks}=1; next; } if ($arg =~ /^-i$/) { $opt{indent}=1; next; } if ($arg =~ /^-n$/) { $opt{num}=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } usage("Too many files specified [$arg and $opt{file}]") if $opt{file}; $opt{file}=$arg; } usage("No file defined") unless $opt{file}; \%opt; } sub debug { return unless $MAIN::DEBUG; foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; } } ################################################## # Main code ################################################## my $HEADING = 0; my $ACTION = 2; my $DIALOGUE = 3; # array of [who,parenthetical,dialogue] my $TRANSITION = 4; my $GENERAL = 5; sub read_script { my ($file) = @_; open(FILE,"<$file") || usage("Couldn't open file: $file"); # Do something to the file my @script; my %alias; while() { chomp; next unless /\S/; next if /^#/; # "post-notes" for comments last if /^ZZSTOP$/; # hook for debugging scripts # Handle {aliases} s/{([^}\s]+)}/$alias{$1} || "{$1}"/eg; if (/^(\S+):=(\S[^\t]*)(\t.*)?$/) { $alias{$1}=$2; } elsif (/^(ext|int)/i) { push(@script, [$HEADING, uc($_)]); } elsif (/([^\t]+?)(\s*\(.+\))?:\t(?:\((.+)\)\s)?(\S.+)/) { my ($name, $vo, $paren, $txt) = ($1, $2, $3, $4); $name = uc($alias{$name} || $name); push(@script, [$DIALOGUE, "$name$vo", $paren, $txt]); } elsif (/^\t(\S.*)/) { push(@script, [$ACTION, $1]); } elsif (/^\t\t(\S.*)/) { push(@script, [$TRANSITION, uc($1)]); } else { push(@script, [$GENERAL, $_]); } } close(FILE); \@script; } sub fold { my ($cols, $txt, $pre, $pre2) = @_; $pre2 = $pre2 || $pre; my @fold; my $at = 0; my $line; while ($txt && $txt =~ s/^(\S*)(\s*)//) { my ($next,$space) = ($1,$2); my $l = length($next); my $ls = length($space); if ($at+$l+$ls < $cols) { $line .= $next.$space; $at+=$l+$ls; } elsif ($at+$l < $cols) { push(@fold, $line.$next); $line=""; $at=0; } elsif ($l > $cols) { push(@fold, $line.substr($next,0,$cols-$at)); while (length($next) > $cols) { push(@fold, substr($next,0,$cols, "")); } $line = $next; $at = length($next); if ($at+$ls < $cols) { $line.=$space; $at+=$ls; } else { push(@fold, $line); $line=""; $at=0; } } elsif ($l+$ls < $cols) { push(@fold, $line); $line=$next.$space; $at=$l+$ls; } else { push(@fold, $line, $next); $line=""; $at=0; } } push(@fold, $line) if $line; my $ret = $pre.join("\n$pre2",@fold); split("\n", $ret); } sub write_script { my ($opt,$script) = @_; my $head = 1; my $tabsize = 5; # tab is 5 chars my $t = " "x$tabsize; # Indent is 2 tabs my $indent = $opt->{indent} ? 2*$tabsize : 0; $indent -= 3 if $indent && $opt->{num}; $indent = " "x$indent; my $line = 1; my $page = 1; my @add; print "PAGE $page:\n"; foreach my $set ( @$script ) { my $what = shift @$set; if ($what == $HEADING) { my $txt = $set->[0]; $txt = "$head $txt" if $opt->{count_head}; $head++; @add = ("", fold(61,$txt)); } elsif ($what == $ACTION) { @add = ("", fold(61,$set->[0])); } elsif ($what == $GENERAL) { @add = fold(78,$set->[0]); } elsif ($what == $TRANSITION) { @add = fold(16,$set->[0],"$t"x8); } elsif ($what == $DIALOGUE) { my ($name,$paren,$txt) = (@$set); @add = ("", fold(38,$name,"$t"x4)); push(@add, fold(24,"$paren)","$t$t$t(","$t$t$t ")) if $paren; push(@add, fold(35,$txt,"$t$t")) if $txt; } if ($opt->{page_breaks} && $line + $#add+1 > $opt->{per_page}) { print " "x50,"(CONTINUED)\n \nCONTINUED"; print " PAGE $page" if $opt->{num}; print ":\n"; $line = 1; $page++; } foreach ( @add ) { printf "%2d ",$line if $opt->{num} && /\S/; printf "",$line if $opt->{num}; print "$indent$_\n"; $line++; } } } sub main { my $opt = parse_args(); my $script = read_script($opt->{file}); write_script($opt,$script); } main(); ################################################## # POD/man ################################################## __END__ =pod =head1 NAME scurvy - Format scripts / screenplays =head1 SYNOPSIS B [S>] =head1 DESCRIPTION scurvy converts text files in a simple format into proper screenplay format. It's something I wrote because I hate using snifty GUI editors when I believe a text editor is all you need. "If you can't vi it, it sucks" It takes a text file as input and outputs a screenplay. More formats may occur someday.. =head1 OPTIONS =over 4 =item B<-c> Number the scene headings (INT/EXT) =item B<-C> Show the "CONTINUED" page breaks =item B<-i> Add the left margin indentation. (Good for final print) =item B<-n> Show page/line numbers =back =head1 FORMAT There are five types of formats: heading, action, dialogue, transition, general. Each type B be on it's own line. (Use I<:set wrap> in vi/vim to make it easier to edit) =over 4 =item B Scene headings are automatically recognized since they start with INT or EXT. =item B Action lines start after one tab. =item B Transition lines start after two tabs =item B Dialogue follows the characters name, a colon and a tab. Some examples: Dave: I think we should go shopping! God (V.O.): That's a bad idea, Dave Dave: (pondering) You're probably right. Parentheticals go after the colon, but V.O., O.S. go before. =item B Generals are just regular text not prefaced by tabs. =item B Any line that starts with a '#' character is ignored. =back =head1 ALIASES Aliases for characters can be defined on any line: D:=Dave And then they can be used as the character speaking dialogue: D: I think we should go shopping! Or in any line of text if inside {curly braces} God (V.O.): That's a bad idea, {D} =head1 EXAMPLE Here's an example input file: D:=Dave (aliases for characters look like this) INT. SCENE HEADING - DAY Actions have one tab Transitions have two tabs General text is just plain text. Dave: dialogue follows the ":" John (V.O.): voice overs go before the : D: (using an alias!) And parentheticals go after! =head1 BUGS Garbage in, garbage out. =head1 AUTHOR David Ljung Madison =cut