#!/usr/bin/perl # Filename: scurvy # Author: David Ljung Madison # From: http://MarginalHacks.com/Hacks/scurvy/ # See License: http://MarginalHacks.com/License/ my $VERSION= '1.02'; # Description: Screenplay/screenwriting tool: txt->script formatter # Also See: http://screenplay.sourceforge.net/ # # Make sure you have not set :expandtab in vi/vim! # Fix with :noexpandtab # use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my $TABSIZE = 5; ################################################## # 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 < [-o ] \tFormats a script \t-d Set debug mode \t-o Output file (default is STDOUT) \t-c Count headings \t-C Show \"Continued\" page breaks \t-i Add initial indent \t-indent Add initial indent [default 2 tabs] \t-in_indent Input indent (for non-scurvy formats) \t-n Show page/line numbers \t-I Set input format [default scurvy] \t-O Set output format [default script] \tFormats: \t scurvy Our simple input format (described in docs) \t text The default text script output \t final Final Draft format (text-with-layout) \t rtf RTF (input only) format - using Final Draft RTF format USAGE exit -1; } sub parse_args { my $opt = {}; $opt->{infmt}='scurvy'; $opt->{outfmt}='text'; # Defaults $opt->{per_page} = 53; my @formats = qw(scurvy text final rtf); while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-o$/) { $opt->{out}=shift @ARGV; next; } if ($arg =~ /^-c$/) { $opt->{count_head}=1; next; } if ($arg =~ /^-C$/) { $opt->{page_breaks}=1; next; } if ($arg =~ /^-i$/) { $opt->{indent}=2; next; } if ($arg =~ /^-indent$/) { $opt->{indent}=shift @ARGV; next; } if ($arg =~ /^-in_indent$/) { $opt->{in_indent}=shift @ARGV; next; } if ($arg =~ /^-I$/) { $opt->{infmt}=shift @ARGV; next; } if ($arg =~ /^-O$/) { $opt->{outfmt}=shift @ARGV; next; } if ($arg =~ /^-n$/) { $opt->{num}=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } usage("Too many files specified [$arg and $opt->{in}]") if $opt->{in}; $opt->{in}=$arg; } usage("No file defined") unless $opt->{in}; usage("Unknown input format: $opt->{infmt}") unless grep($opt->{infmt} eq $_, @formats); usage("Unknown output format: $opt->{outfmt}") unless grep($opt->{outfmt} eq $_, @formats); usage("Can't output rtf format: $opt->{outfmt}") if $opt->{outfmt} eq 'rtf'; $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 add { my ($script,$type,@what) = @_; push(@$script, [$type, @what]); } sub read_scurvy { my ($opt) = @_; # Read in a scurvy format 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} # ignore:{ s/{([^}\s]+)}/$alias{$1} || "{$1}"/eg; if (/^(\S+):=(\S[^\t]*)(\t.*)?$/) { $alias{$1}=$2; } elsif (/^(ext|int|i\/e)/i) { add($script, $HEADING, uc($_)); } elsif (/([^\t]+?)(\s*\(.+\))?:\t(?:\((.+)\)\s+)?(\S.+)/) { my ($name, $vo, $paren, $txt) = ($1, $2, $3, $4); $name = uc($alias{$name} || $name); add($script, $DIALOGUE, "$name$vo", $paren, $txt); } elsif (/^\t(\S.*)/) { add($script, $ACTION, $1); } elsif (/^\t\t(\S.*)/) { add($script, $TRANSITION, uc($1)); } else { add($script,$GENERAL, $_); } } $script; } my $saveline = undef; sub getline { my ($opt) = @_; if (defined $saveline) { $_ = $saveline; undef $saveline; return $_; } $_ = scalar ; return $_; } sub pushline { my ($opt,$line) = @_; die("INTERNAL ERROR: Can't pushline twice without calling getline!\n") if defined $saveline; $saveline = $line; } sub cutline { my ($opt) = @_; s/[\r\n]$//g; # Chomp, but handles DOS format better s/^[ \d]\d / /g; # Remove line numbering next if /^PAGE \d+:$/; # Remove page numbering next if /^\s+\(CONTINUED\)$/; # Remove continuation lines next if /^CONTINUED( PAGE \d+:)?$/; # Figure out how many tabs of indentation we have /^(\s*)(.*)/; my ($indent,$txt) = (length($1),$2); $indent++ if $txt =~ /^\(/; # Final Draft puts the '(' in the indent my $tabs = int($indent/$TABSIZE) - $opt->{in_indent}; $tabs=0 if $tabs<0; ($tabs,$txt); } # Keep getting lines as long as they have the right number of tabs sub continuetabs { my ($opt,$from,$to) = @_; $to = $to || $from; my @ret; while(getline($opt)) { my $line = $_; my ($tabs,$txt) = cutline($opt); if ($tabs<$from || $tabs>$to) { pushline($opt,$line); last; } $txt =~ s/\s$//; push(@ret,$txt); } join(' ',@ret); } sub read_text { my ($opt) = @_; my $script; # Read in a plaintext script my $script = []; while(getline($opt)) { next unless /\S/; my ($tabs,$txt) = cutline($opt); if ($tabs==0) { # Heading, Action or General if ($txt =~ /^(ext|int|i\/e)/i) { add($script, $HEADING, $txt); next; } # Could either be action or general.. # go with action since it has a shorter right margin # We have no way of telling if the next lines are a general # or a continuation of an action.. add($script, $ACTION, $txt); } elsif ($tabs==4) { my ($name,$paren,$dialogue) = $txt; my $paren = continuetabs($opt,3); $paren =~ s/^\(//; $paren =~ s/\)$//; my $dialogue = continuetabs($opt,2); add($script, $DIALOGUE, $name, $paren, $dialogue); } elsif ($tabs>=8) { my $and = continuetabs($opt,8,10); $txt .= " $and" if $and; $txt =~ s/:$//; add($script, $TRANSITION, $txt); } else { usage("Saw an unexpected spacing [$tabs tabs] in input file.\n\tConsider using -in_indent option"); } } $script; } sub read_rtf { my ($opt) = @_; my $script; my @styles = qw(GENERAL SCENE_HEADING ACTION CHARACTER_NAME PARENTHETICAL DIALOG TRANSITION SHOT); # Read in a Final Draft RTF file my $script = []; my %style; my ($name,$paren); while () { chomp; if (/^{\\s(\d+)[^}]+?\s+([^\\][^\}]+);\s*}/) { my ($type,$num) = (uc($2),$1); $type =~ s/\s+/_/g; $style{$num} = $type; print STDERR "Unknown style type: $type\n" unless grep($type eq $_, @styles); } if (/{\\pard.*\\s(\d+)[^}]+?\s+([^\\][^\}]+)\\par\s*}/) { my ($style,$stylenum,$txt) = ($style{$1},$1,$2); if (!$style) { print STDERR "Unknown style: \\s$stylenum\n"; next; } # These are some RTF text codes I know. I doubt script writers # even use all of these. Does anyone know how you put { or } in text? $txt =~ s/\\tab/\t/g; $txt =~ s/\\emdash/--/g; $txt =~ s/\\endash/-/g; $txt =~ s/\\e(m|n)space/ /g; $txt =~ s/\\\~/ /g; $txt =~ s/\\_/-/g; $txt =~ s/\\'([0-9a-f]{2})/chr(hex("0x$1"))/eig; $txt =~ s/\\(l|r)quote/'/g; $txt =~ s/\\(l|r)dblquote/"/g; my @txt = split(/\\(?:par|sect|page)\s*/, $txt); my $simple; $simple = $HEADING if $style eq 'SCENE_HEADING'; $simple = $ACTION if $style eq 'ACTION'; $simple = $GENERAL if $style eq 'GENERAL'; $simple = $TRANSITION if $style eq 'TRANSITION'; # Not sure what to do with this, we don't handle shots. $simple = $TRANSITION if $style eq 'SHOT'; if (defined $simple) { map add($script, $simple, $_), @txt; next; } if ($style eq 'CHARACTER_NAME') { $name = $txt; next; } if ($style eq 'PARENTHETICAL') { $paren = $txt; $paren =~ s/^\(//; $paren =~ s/\)$//; next; } if ($style eq 'DIALOG') { foreach my $t ( @txt ) { add($script, $DIALOGUE, $name, $paren, $t); undef $paren; } undef $name; next; } print STDERR "Unused/unknown style? [$style,$stylenum]\n"; } } $script; } sub read_input { my ($opt) = @_; open(IN,"<$opt->{in}") || usage("Couldn't read file: $opt->{in}"); my $script; if ($opt->{infmt} eq 'scurvy') { $script = read_scurvy($opt); } elsif ($opt->{infmt} eq 'text') { $script = read_text($opt); } elsif ($opt->{infmt} eq 'final') { $opt->{in_indent} = 3 if !defined $opt->{in_indent}; $script = read_text($opt); } elsif ($opt->{infmt} eq 'rtf') { $script = read_rtf($opt); } close(IN); $script; } ################################################## # Output ################################################## 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_scurvy { my ($opt,$script) = @_; my $t = "\t"; foreach my $set ( @$script ) { my $what = shift @$set; if ($what == $DIALOGUE) { my ($name,$paren,$txt) = (@$set); $txt = "($paren) $txt" if $paren; print OUT "\n"; print OUT "${name}:\t$txt\n"; next; } my $tabs=0; $tabs=0 if $what == $HEADING; $tabs=1 if $what == $ACTION; $tabs=0 if $what == $GENERAL; $tabs=2 if $what == $TRANSITION; print OUT ${t}x$tabs.join('',@$set)."\n"; } print OUT "\n"; } sub write_text { my ($opt,$script) = @_; my $head = 1; my $tabsize = $TABSIZE; my $t = " "x$tabsize; # Indent is 2 tabs my $indent = $opt->{indent} ? $opt->{indent}*$tabsize : 0; $indent -= 3 if $indent && $opt->{num}; $indent = " "x$indent; my $line = 1; my $page = 1; my @add; my $last_paren = undef; print OUT "PAGE $page:\n" if $opt->{num}; 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 = $last_paren eq $name ? () : ("", 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; #$last_paren=$name; } if ($opt->{page_breaks} && $line + $#add+1 > $opt->{per_page}) { print OUT " "x50,"(CONTINUED)\n \nCONTINUED"; print OUT " PAGE $page" if $opt->{num}; print OUT ":\n"; $line = 1; $page++; } foreach ( @add ) { printf OUT "%2d ",$line if $opt->{num} && /\S/; printf OUT "",$line if $opt->{num}; print OUT "$indent$_\n"; $line++; } } } sub write_output { my ($opt,$script) = @_; my $out = $opt->{out} || '&STDOUT'; open(OUT, ">$out") || usage("Couldn't write output? [$out]"); if ($opt->{outfmt} eq 'scurvy') { write_scurvy($opt,$script); } elsif ($opt->{outfmt} eq 'text') { write_text($opt,$script); } elsif ($opt->{outfmt} eq 'final') { $opt->{indent} = 3 if !defined $opt->{indent}; write_text($opt,$script); } close OUT; } sub main { my $opt = parse_args(); debug("Version: $VERSION\n"); my $script = read_input($opt); write_output($opt,$script); } main(); ################################################## # POD/man ################################################## __END__ =pod =head1 NAME scurvy - Format scripts / screenplays =head1 SYNOPSIS B [S] EIE [SIE>>] =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<-out> I Set the output file (otherwise write to standard out) =item B<-c> Number the scene headings (INT, EXT, I/E) =item B<-C> Show the "CONTINUED" page breaks =item B<-i> Add the left margin indentation. (Good for final print) =item B<-indent> I Add the left margin indentation with a specific number of tabs. =item B<-in_indent> I Specify the extra number of indent tabs in the input file. (Defaults to three for 'final' input format) =item B<-n> Show page/line numbers =item B<-I> I Specify the input format: B Our simple input format (described below) B The default text script output (can be an input format also) B Final Draft format (text-with-layout) B RTF (input only) format Using the Final Draft style sheet with styles: GENERAL SCENE HEADING ACTION CHARACTER NAME PARENTHETICAL DIALOG TRANSITION SHOT If you want to convert a Final Draft document so you can edit it in scurvy, then first save it as text-with-layout - we'll call this file 'mymovie.fdr' Then you can use scurvy to convert this to scurvy format: % scurvy -I final mymovie.fdr -O scurvy -o mymovie.scr It's possible you'll get an error if your margins are different - the default margins should be read correctly, but if not try adjusting -in_indent. You can also try saving as RTF and then reading in the RTF, though this isn't guaranteed to work with all versions of Final Draft. If you have a different script writer that does RTF output, I'd love to see a sample copy. To convert rtf we would save as rtf and: % scurvy -I rtf mymovie.rtf -O scurvy -o mymovie.scr Then you can edit mymovie.scr and get the final output with scurvy: % scurvy mymovie.scr -o mymovie.txt You will, of course, lose any aliases you may have had. Try a simple search and replace. For example, to convert the name "Dave" to be an alias "D" you can add the alias line to the top: Dv:=Dave Madison And then do the search and replace (example using vi/vim): :%s/^Dave Madison/Dv:/ :%s/^Dave Madison (/Dv (/ (The second line is for quotes with parenthesis) =item B<-O> I Specify the output format. (Same formats as input) =back =head1 REQUIREMENTS Since scurvy uses tabs to determine formatting, you need to make sure your text editor isn't converting this into spaces. In vi and vim you need to make sure you haven't set 'expandtab' - if you have, you can turn it off with :noexpandtab =head1 SCURVY FORMAT There are five types of line 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, EXT or I/E. =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. When reading 'final' or 'text' formats, it's impossible to differentiate between a new 'GENERAL' line or a continuation of an 'ACTION' line since they look the same. Sad but true. Hence I assume them to all be ACTIONs. The different format parsing/output is somewhat beta - if you find any bugs please send me an example script and the problem. =head1 AUTHOR David Ljung Madison =cut