PerlMonks Editor
GrandFather
created: 2006-04-13 18:08:22

This is the first cut of a PerlMonks offline wysiwyg editor. It may be used to wysiwig edit material for posting on PerlMonks and generates the common HTML and PerlMonks special tags used in PerlMonks node markup.

The code uses Tk and should be reasonably cross platform.

This first cut is missing a lot of functionality but is being posted to garner initial reactions and make it available to the curious (I'm off on holiday for a few days and won't be able to work on it for another week).

Important missing stuff includes - no readmore handling, save/open partially implemented (and disabled), keyboard accelerators not working (and disabled), much markup not hooked up.

use strict;
use warnings;
use diagnostics;

use Tk;
#use Tk::TextUndo;
use Tk::Balloon;
use Tk::Clipboard;
use Tk::FBox;
use Clone qw(clone);

=head Todo

Provide file open and save
Manage B, F, I, P, R and U flags
Translate entities (note U flag)
Handle accelerator keys
Support snippet text (with formatting)
Disallow relink
Checkmarks for menus
handle tables
hook up keys
Add Edit menu to access find and replace etc

=cut

use constant kParaSpace => 4;

my $currentFile = '';
my %tagTypes;       # Style tag data
my %formatFonts;    # Fonts used in style tags. Keyed by tag
my %bindings;       # Key, menu and toolbar bindings. Tag is value
my %menuItems;      # Child menu widgets keyed by menu label path
my %entities =      # Entities we need to use outside code blocks
    (
    '&', '&',
    '<', '<',
    '>', '>',
    '[', '[',
    ']', ']',
    );
my @stdFlags = (
    'B', # Block level element
    'C', # Clear all or specified tags: C or Ctag (note lower case)
    'F', # Format tag (inline element)
    'I', # Item in a list. Implies B
    'L', # Link
    'P', # Applies to whole paragraph
    'R', # Readmore text
    'S', # Single spaced text
    'U', # Untranslated - don't translate entities
    'X', # Exclude all or specified tags: X or Xtag (note lower case)
    );

while () {
    # Load the default configuration stuff
    chomp;
    next if ! length;
    last if /^#key /;
    next if /^#/;
    
    my ($tag, $htmlTag, $name, $flagsField, @options) = split /\s*,\s*/;

    (print "Missing entries in tag line ($.): $_"), next if ! defined $flagsField;

    # pull out flags and handle X and C special case flags
    my %flags;
    @flags{@stdFlags} = (0) x @stdFlags; # Preset flags off
    $flags{'C'} = {};
    $flags{'X'} = {};

    for (split /(?=[A-Z][a-z]*)/, $flagsField) {
        my ($flag, $value) = split /(?<=[A-Z])/, $_;
        
        print "Unhandled flag '$flag' used\n" if ! exists $flags{$flag};
        if (-1 != index 'XC', $flag) {
            $flags{$flag}{$value || 'ALL'} = 1;
            $flags{'C'}{$value || 'ALL'} = 1 if $flag eq 'X'; # X implies C
        } else {
            $flags{$flag} = $value || 1;
            $flags{'B'} = $value || 1 if $flag eq 'I';
        }
    }

    #Fix up options
    my $optionStr = join ', ', @options;
    my %optionHash;
    
    while ($optionStr =~ /\G,?\s*((?:(?!=>).)*)=>\s*(\[[^\]]*\]|[^,]*),?\s*/g) {
        my ($option, $value) = ($1, $2);
        
        trim (\$option, \$value);
        
        if ($value =~ s/\[|\]//g) {
            # Nested options. Turn them into a hash
            my @options = split ',', $value;
            my %optionHash;
            
            for (@options) {
                my ($suboption, $subvalue) = split /\s*=>\s*/;
                
                last if ! defined $subvalue;
                trim (\$suboption, \$subvalue);
                $optionHash{$suboption} = $subvalue;
            }
            
            $value = \%optionHash;
        }
        
        $optionHash{$option} = $value;
    }
    
    $tagTypes{$tag} = [$htmlTag, $name, \%flags, \%optionHash];
}

while () {
    # Load key binding information
    next if /^#/;
    chomp;
    next if ! length;
    
    my ($tag, $key, $menuItem, $toolbarItem, $rightClickItem) = split /\s*,\s*/;
    (print "Missing tag in binding line ($.): $_"), next if ! defined $tag;
    $bindings{$tag} = [$key, $menuItem, $toolbarItem, $rightClickItem];
}

my $mw = MainWindow->new (-title => "PerlMonks node editor");
my $text = $mw->Scrolled
    ('Text', -font => 'normal', -wrap => 'word', -scrollbars => 'e',);

my $status = $mw->Label(-width => 60, -relief => "sunken", -bd => 1, -anchor => 'w');
my $balloon = $mw->Balloon(-statusbar => $status);
my $msg = '';
my $balloonCharIndex = '';
my $balloonLastIndex = '';

$status->pack(-side => "bottom", -fill => "both", -padx => 2, -pady => 1);
#$balloon->attach
#    (
#    $text, -msg => \$msg,
#    -balloonposition => 'mouse',  # Not really used since the postcommand returns the real position.
#    -postcommand => \&balloonPostCommand,
#    -motioncommand => \&balloonMotionCommand,
#    );

my $menuBar = $mw->Menu (-type => 'menubar');

$mw->configure(-menu => $menuBar);
$text->pack (-expand => 'yes', -fill => 'both');

# Build file menu
$menuItems{'~File'} = $menuBar->cascade(-label => '~File', -tearoff => 0);
$menuItems{'~File'}->command (-label => '~Render', -command => \&fileRender);
#$menuItems{'~File'}->command (-label => '~Open...', -command => \&fileOpen);
#$menuItems{'~File'}->command (-label => '~Save', -command => \&fileSave);
#$menuItems{'~File'}->command (-label => 'Save ~As...', -command => \&fileSaveAs);
$menuItems{'~File'}->command (-label => 'E~xit', -command => \&fileExit);

# Build menus and bind keys
for my $tag (keys %bindings) {
    my $menuPath = $bindings{$tag}[1];

    next if ! defined $menuPath;

    my ($top, $item) = split '/', $menuPath;
    
    next if ! defined $item;
    if (! defined $menuItems{$top}) {
        $menuItems{$top} = $menuBar->cascade(-label => $top, -tearoff => 0);
    }
    
    my $newItem = $menuItems{$top}->command
        (-label => $item, -command => [\&doCommand, $tag]);
    
    if (defined $bindings{$tag}[0]) {
        #Set up accelerator bindings
        
        #my $key = $bindings{$tag}[0];
        #
        #$mw->bind ("<$key>" => [\&keyCommand, $tag]);
        #
        #$key =~ s/^Control/ctrl/;
        #$newItem->configure (-accelerator => $key);
    }
}

$menuItems{'~Help'} = $menuBar->cascade(-label => '~Help', -tearoff => 0);
$menuItems{'~Help'}->command (-label => '~PerlMonks Editor Help', -command => \&help);
$menuItems{'~Help'}->command (-label => '~About', -command => \&about);

# A couple of phantom paragraph spacing tags to ease calculating paragraph spacing
$text->tagConfigure("!para_start", -spacing1 => 0, -spacing3 => -(kParaSpace));
$text->tagConfigure("!para_end", -spacing1 => -(kParaSpace), -spacing3 => 0);

$text->insert ('end', "Some text to play with.\n", '!para');
$text->insert ('end', "Some more text to play with. Some more text to play with.\n", '!para');
#$mw->bind ("<$key>" => [\&keyCommand, $tag]);

MainLoop ();

sub balloonPostCommand {
    return 0 if ! length $balloonCharIndex;
    
    my %balloonCharTags;
    my  $charIndex = $text->index ("$balloonCharIndex +1 char");
    
    @balloonCharTags{$text->tagNames()} = ($balloonCharIndex);
    
    # If no tags under mouse don't post the balloon.
    return 0 if ! %balloonCharTags;
    
    if (exists $balloonCharTags{name}) {
        my ($start, $end) = $text->tagPrevrange ('name', $balloonCharIndex);
        my $name = $text->get($start, $end);
        
        $name =~ s/\|.*//;
        $msg = "link to [${name}]'s home node";
    } elsif (exists $balloonCharTags{node}) {
        my ($start, $end) = $text->tagPrevrange ('node', $balloonCharIndex);
        my $node = $text->get($start, $end);
        
        $node =~ s/\|.*//;
        $msg = "link to node id $node";
        $msg .= ' (badly formed - digits only allowed)' if $node !~ /^\d+$/;
    } else {
        return 0;
    }
    
    my @p = $text->bbox($balloonCharIndex);
    my $x = $text->rootx + $p[0] + $p[2] - 4;
    my $y = $text->rooty + $p[1] + $p[3] + 2;
    print "-$x,$y-\n";
    return "$x,$y";
}

sub balloonMotionCommand {
    my $x = $text->pointerx - $text->rootx;
    my $y = $text->pointery - $text->rooty;
    
    $balloonCharIndex = $text->index ("\@$x,$y");

    # If the same char don't cancel the balloon.
    return 0 if $balloonLastIndex eq $balloonCharIndex;
    
    # New char under mouse - cancel it so a new balloon will be posted.
    $balloonLastIndex = $balloonCharIndex;
    print ">$balloonLastIndex<\n";
    return 1;
}

sub fileRender {
    $text->clipboardClear ();
    $text->clipboardAppend (render ());
}

sub fileOpen {
    $currentFile = $text->FBox(-type => 'open', -filter => '*.PMEdit')->Show;
    
    open inFile, '<', $currentFile or
        $text->messageBox
            (
            -title => 'Save failed', -icon => 'error',
            -type => 'Ok',
            -message => "Unable to open '$currentFile' - $!"
            );
    my ($html, $name, $mode, $params);
           
    while () {
        my ($type, $index, $item) = /(\S+)\s(\S+)\s(.*)/;
        
        if ($type =~ /^tago(?:n|ff)$/) {
            next if $item =~ /^_/;
            ($html, $name, $mode, $params) = @{$tagTypes{$item}};
        }

        if ($type eq '-tagon') {
            print outFile "-tagon $item $index\n";
        } elsif ($type eq '-tagoff') {
            print outFile "-tagoff $item $index\n";
        } elsif ($type eq '-text') {
            print outFile "-text $item\n";
        } else {
            print "Token type $type at $index not handled.\n";
        }
    }
    
    close inFile;
}

sub fileSave {
    if (defined $currentFile and length $currentFile) {
        doSave ($currentFile);
    } else {
        fileSaveAs ();
    }
}

sub fileSaveAs {
    my $filename = $text->FBox(-type => 'save', -filter => '*.PMEdit')->Show;
    doSave ($filename);
}

sub doSave {
    my $filename = shift;
    
    return if ! defined $filename or ! length $filename;
    
    open outFile, '>', $filename or
        $text->messageBox
            (
            -title => 'Save failed', -icon => 'error',
            -type => 'Ok',
            -message => "Unable to create '$filename' - $!"
            );
    my @dumpText = $text->dump ('-tag', '-text', 'start', 'end');
    my ($html, $name, $mode, $params);
    
    while (@dumpText) {
        my ($type, $item, $index) = splice @dumpText, 0, 3;
        
        my $segEnd = exists $dumpText[2] ? $dumpText[2] : 'end';

        if ($type =~ /^tago(?:n|ff)$/) {
            next if $item =~ /^_/;
            ($html, $name, $mode, $params) = @{$tagTypes{$item}};
        }

        if ($type eq 'tagon') {
            print outFile "-tagon $index $item\n";
        } elsif ($type eq 'tagoff') {
            print outFile "-tagoff $index $item\n";
        } elsif ($type eq 'text') {
            print outFile "-text - $item\n";
        } else {
            print "Token type $type at $index not handled.\n";
        }
    }
    
    close outFile;
    $currentFile = $filename;
}

sub fileExit {
    exit 1;
}

sub render {
    my $result;
    my $paragraph;
    my $inCode = 0;
    my @dumpText = $text->dump ('-tag', '-text', '1.0', 'end');
    my ($html, $name, $mode, $params);
    
    while (@dumpText) {
        my ($type, $item, $index) = splice @dumpText, 0, 3;
        
        next if $item =~ m'^(?:sel|para)';
        
        my $segEnd = exists $dumpText[2] ? $dumpText[2] : 'end';

        if ($type =~ /^tago(?:n|ff)$/) {
            next if $item =~ /^(?:_|!)/;
            ($html, $name, $mode, $params) = @{$tagTypes{$item}};
        }
            
        if ($type eq 'tagon') {
            if ($mode->{'L'}) {
                my ($linkCode) = $html =~ /^\S*\s*(.*)/;
                $paragraph .= "[$linkCode";
                next;
            }

            $inCode = 1 if $item eq 'code';
            $paragraph .= "<$tagTypes{$item}[0]>";
        } elsif ($type eq 'tagoff') {
            if ($mode->{'L'}) {
                $paragraph .= ']';
                next;
            }
            
            $paragraph .= "";
            if ($item eq 'code') {
                $inCode = 0;
            } else {
            }
        } elsif ($type eq 'text') {
            $paragraph .= $item;

            if ($paragraph =~ /\n/) {
                if ($inCode) {
                    $result .= $paragraph;
                } else {
                    $paragraph =~ tr/\n//d;
                    if ($paragraph eq '
') { $result .= $paragraph; $paragraph = ''; } $result .= "

$paragraph

\n"; } $paragraph = ''; } } else { print "Token type $type at $index not handled.\n"; } } $result =~ s|

|
|g; return $result; } sub keyCommand { &doCommand (); } sub doCommand { my %newTag = (tag => shift); my @selections = $text->tagRanges('sel'); @newTag{'name', 'html', 'flags', 'params'} = @{$tagTypes{$newTag{tag}}}; do { if (@selections) { my %tags; @tags{$text->tagNames($selections[0])} = (); # Preset current tags $newTag{isOn} = ! exists $tags{$newTag{tag}}; # Complement new tag's curr state $tags{$newTag{tag}} ||= $newTag{isOn}; @newTag{'start', 'end'} = splice @selections, 0, 2; } else { my %activeTags; @activeTags{$text->tagNames('insert')} = (); return if ! exists $activeTags{$newTag{tag}}; @newTag{'start', 'end'} = $text->tagPrevrange ($newTag{tag}, 'insert'); $newTag{isOn} = 0; } return if ! defined $newTag{end}; my $msg = $newTag{flags}{L} ? manageLink (%newTag) : updateTextTags (%newTag); if (length $msg) { $status->configure (-text => $msg); return; } } while (@selections); } sub updateTextTags { my %newTag = @_; my @dumpText = $text->dump ('-tag', '-text', $newTag{start}, $newTag{end}); my @activeTags = $text->tagNames($newTag{start}); my %tags; @tags{@activeTags} = (1) x @activeTags; # Preset current tags $tags{$newTag{tag}} = $newTag{isOn}; TOKEN: while (@dumpText) { my ($type, $item, $index) = splice @dumpText, 0, 3; my $segEnd = exists $dumpText[2] ? $dumpText[2] : $newTag{end}; if ($type eq 'tagon') { $tags{$item} = 1 if $item ne $newTag{tag}; } elsif ($type eq 'tagoff') { $tags{$item} = 0 if $item ne $newTag{tag}; } elsif ($type eq 'text') { my @tagList = grep {! /^_|^sel$/ && $tags{$_}} keys %tags; my @removeList = grep {! $tags{$_} || /^_/} keys %tags; # Bail if current tags preclude new tag for (@tagList) { next if ! exists $tagTypes{$_} or $newTag{tag} eq $_; my ($Ihtml, $Iname, $Iflags, $Iparams) = @{$tagTypes{$_}}; # Check for existing tag that precludes all new tags if ($Iflags->{'X'}{'ALL'}) { next TOKEN } # Check for existing tag that precludes $newTag if ($Iflags->{'X'}{$newTag{tag}}) { next TOKEN; } } if ($newTag{isOn}) { if ($newTag{flags}->{'C'}{'ALL'}) { # Strip all other tags push @removeList, @tagList; } elsif (%{$newTag{flags}->{'C'}}) { # Clear specific tags push @removeList, keys %{$newTag{flags}->{'C'}}; } push @tagList, $newTag{tag}; } $text->tagRemove ($_, $index, $segEnd) for @removeList; @tagList = buildTag (@tagList); $text->tagAdd ($_, $index, $segEnd) for @tagList; fixParaSpacing ($index); } else { print "Token type $type at $index not handled.\n"; } } return ''; } sub manageLink { my %newTag = @_; my @activeTags = $text->tagNames($newTag{start}); my %tags; if (! $newTag{isOn}) { # Remove the link $text->tagRemove ($newTag{tag}, $newTag{start}, $newTag{end}); updateTextTags (%newTag); return ''; } @tags{@activeTags} = (1) x @activeTags; # Preset current tags for (keys %tags) { next if ! exists $tagTypes{$_}; return 1 if $newTag{tag} eq $_ and $newTag{isOn}; # Link already my ($Ihtml, $Iname, $Iflags, $Iparams) = @{$tagTypes{$_}}; return "Can't link inside $Iname" if $Iflags->{'X'}{'ALL'}; return "Can't link inside $Iname" if $Iflags->{'X'}{'link'}; } return 'Links must not span line ends.' if int ($newTag{start}) != int ($newTag{end}); # Get the link text my $orgLinkText = $text->get($newTag{start}, $newTag{end}); my ($linkStr, $textStr) = $orgLinkText =~ /^([~|]*\|?)(.*)/; my $indexStr = "$newTag{start} +" . length ($linkStr) . 'chars'; my $linkEnd = $text->index ($indexStr); my %linkTag = %{clone (\%newTag)}; my %textTag = %{clone (\%newTag)}; $linkTag{end} = $linkEnd; $textTag{start} = $linkEnd; updateTextTags (%linkTag); updateTextTags (%textTag); return ''; } sub buildTag { my %tags; @tags{@_} = (); my @tagList = sort keys %tags; my $newFormatTag = '_' . join '_', @tagList; my %options; my %fontParams; for (@tagList) { next if ! exists $tagTypes{$_} || ! ref $tagTypes{$_}; my ($html, $name, $mode, $params) = @{$tagTypes{$_}}; next if ! ref $params; for my $type (keys %$params) { if ($type =~ /-font/) { for my $subType (keys %{$params->{$type}}) { $fontParams{$subType} = $params->{$type}{$subType}; } } else { $options{$type} = $params->{$type}; } } } $options{-font} = buildFont (%fontParams) if %fontParams; $text->tagConfigure ($newFormatTag, %options); push @tagList, $newFormatTag; return @tagList; } sub buildFont { my %options = @_; my $fontName = ''; $fontName .= "$_|$options{$_}," for sort keys %options; $fontName =~ tr/-+/mp/; $fontName =~ tr/a-zA-Z0-9/mp_/c; $mw->fontCreate($fontName, %options) if ! $formatFonts{$fontName}++; return $fontName; } sub fixParaSpacing { my $lastLine = ($text->index ('end') =~ /(\d+)/)[0]; my $lastTailSpace = -(kParaSpace); my @paraTags; push @paraTags, "!para_$_" for (1..$lastLine); $text->tagDelete (@paraTags); # Clear current spacing tags for my $line (1..$lastLine) { my $headSpace = kParaSpace; my $tailSpace = kParaSpace; my @activeTags = $text->tagNames("$line.0"); # Note that this is currently broken if the first character happens to be a # part of a single spaced style applied to a partial line for (@activeTags) { next if ! exists $tagTypes{$_} || ! ref $tagTypes{$_}; my ($html, $name, $mode, $params) = @{$tagTypes{$_}}; next if ! ref $params; for my $type (keys %$params) { $headSpace = $params->{$type} if $headSpace && $type =~ /-spacing1/; $tailSpace = $params->{$type} if $tailSpace && $type =~ /-spacing3/; } } if ($lastTailSpace == -(kParaSpace)) { $headSpace = 0; } elsif ($lastTailSpace == 0 && $headSpace > 0) { $headSpace += kParaSpace; } elsif ($lastTailSpace > 0 && $headSpace == 0) { $headSpace += kParaSpace; } $text->tagConfigure("!para_$line", -spacing1 => $headSpace, -spacing3 => $tailSpace); $text->tagAdd ("!para_$line", "$line.0"); $text->tagRaise ("!para_$line"); $lastTailSpace = $tailSpace; } } sub trim { for (@_) { $$_ =~ s/^\s+//; $$_ =~ s/\s+$//; } } sub help { my $msg = <messageBox ( -icon => 'info', -message => $msg, -title => 'PerlMonks Editor Help', -type => 'Ok', ); } sub about { my $msg = <messageBox ( -icon => 'info', -message => $msg, -title => 'About PerlMonks Editor', -type => 'Ok', ); } __DATA__ #tag style definitions #tag name,HTML tag, UI text, flags, modifiers as key value pairs big,big,Big font,F,-font => [-size => 16] bold,b,Bold,F,-font => [-weight => bold] center,center,Centered text,P, code,code,Code block,BFXCU,-spacing1 => 0,-spacing3 => 0,-background => #e0e0ff,-font => [-family => courier, -weight => bold] cpan,link id://,CPAN link,L, -background => #c0c0c0, -foreground => #40e040, dd,dd,Definition Description,B, del,del,Deleted Text,F, dl,dl,Definition List,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m dt,dt,Definition Term,B,-lmargin1 => 10m, -lmargin2 => 10m, -rmargin => 10m, -font => [-weight => bold] emphasis,em,Emphasis,F,-font => [-weight => bold] h3,h3,Header level 3,B,-font => [-size => 24], -background => #c0c0c0,-spacing1 => 14 h4,h4,Header level 4,B,-font => [-size => 24], -background => #8080c0,-spacing1 => 10 h5,h5,Header level 5,B,-font => [-size => 16], -background => #c0c0c0,-spacing1 => 10 h6,h6,Header level 6,B,-font => [-size => 16], -background => #8080c0,-spacing1 => 8 hrule,hr,Horizontal rule,BX, inserted,ins,ins,BF, -background => #ffffc0, italic,i,Italic,F,-font => [-slant => italic] item,li,List item,I, olist,ol,Ordered list,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m quote,blockquote,Quoted block,P,-lmargin1 => 15m,-lmargin2 => 15m,-rmargin => 15m readmore,readmore,Read more block,BR,-background => #a0b7ce small,small,small,F,-font => [-size => 8] spoiler,spoiler,Spoiler,F, -background => #000000, -foreground => #404040, strike,strike,Strike Out,F,-overstrike => on strong,strong,Strong emphasis,F, sub,sub,Sub script,FCsuper,-offset => -2p,-font => [-size => 8] super,sup,Super script,FCsub,-offset => 4p,-font => [-size => 8] teletype,Teletype text,tt,F,-font => [-family => courier], -background => #FFFFc0 ulist,ul,Unordered list,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m underline,u,Underline,F,[-underline => on]], #links - still tag style definitions acronym,link acronym://,Acronym link,L, -background => #f0f0f0, -foreground => #0060c0, cpan,link cpan://,Cpan link,L, -background => #f0f0f0, -foreground => #00a0a0, dict,link dict://,Dictionary link,L, -background => #f0f0f0, -foreground => #00a0a0, dist,link dist://,CPAN Distro link,L, -background => #f0f0f0, -foreground => #00a0a0, doc,link doc://,perldoc link,L, -background => #f0f0f0, -foreground => #00a0a0, ftp,link ftp://,Ftp link,L, -background => #f0f0f0, -foreground => #00a0a0, google,link google://,Google link,L, -background => #f0f0f0, -foreground => #00a0a0, href,link href://,Href link,L, -background => #f0f0f0, -foreground => #00a0a0, http,link http://,Http link,L, -background => #f0f0f0, -foreground => #00a0a0, https,link https://,Https link,L, -background => #f0f0f0, -foreground => #00a0a0, id,link id://,Node id link,L, -background => #f0f0f0, -foreground => #00a0a0, isbn,link isbn://,Isbn link,L, -background => #f0f0f0, -foreground => #00a0a0, jargon,link jargon://,Jargon link,L, -background => #f0f0f0, -foreground => #00a0a0, kobes,link kobes://,Kobes link,L, -background => #f0f0f0, -foreground => #00a0a0, lj,link lj://,Live journal link,L, -background => #f0f0f0, -foreground => #00a0a0, lucky,link lucky://,Google lucky link,L, -background => #f0f0f0, -foreground => #00a0a0, mod,link mod://,Mod link,L, -background => #f0f0f0, -foreground => #00a0a0, module,link module://,Module link,L, -background => #f0f0f0, -foreground => #00a0a0, name,link,Node name link,L, -background => #f0f0f0, -foreground => #0060c0, pad,link pad://,Scratchpad link,L, -background => #f0f0f0, -foreground => #00a0a0, perldoc,link perldoc://,Perldoc link,L, -background => #f0f0f0, -foreground => #00a0a0, pmdev,link pmdev://,Pmdev link,L, -background => #f0f0f0, -foreground => #00a0a0, wp,link wp://,Wp link,L, -background => #f0f0f0, -foreground => #00a0a0, #key bindings, menu items and tool bar items #tag,key,menu item,toolbar item,right click item big,Control 2,Format/Big,,Big bold,Control Shift b,Format/Bold,,Bold italic,Control i,Format/Italic,,Italic strike,Control s,Format/Strike out,,Strike out sub,Control u,Format/Subscript,,Subscript super,Control s,Format/Superscript,,Superscript code,Control c,Format/Code,,Code id,,Links/Node,,Node id link name,,Links/Name,,Name link

DWIM is Perl's answer to Gödel
Re: PerlMonks Editor
created: 2006-04-14 02:44:19

Cool stuff++

After spending 15 minutes writing up a node this morning, and then managing to blow it away by mis-advertently closing the wrong browser tab - (UGH!) - this has come just at the right time :)

One minor asthetic request - the "Format" menu items are obviously toggles. It would be nice to have them "ticked" (or not) as they've been applied.

And one minor annoyance - it seems to convert any linebreak into

. Wouldn't it be more appropriate (and intuitive) if it only started a new paragraph where there was a blank line separating blocks of text, and convert all other line breaks into
?

Example:

This is a line, with a linebreak
And another line directly underneath

# Converts to:

This is a line, with a linebreak
And another line directly underneath

# Where as: Blah blah some text and random pontification that goes on for a few lines. Then a new paragraph starts here... # Converts to:

Blah blah some text and random pontification that goes on for a few lines.

Then a new paragraph starts here...

Cheers,
Darren :)

Re^2: PerlMonks Editor
created: 2006-04-14 11:21:11

...blow it away by mis-advertently closing the wrong browser tab...
The coolness of this program notwithstanding, you do know about Undo close tab extension for Firefox, no?

Re^3: PerlMonks Editor
created: 2006-04-14 11:34:19
No, I didn't (obviously :p)
But I do now, thank you very much :)
Re^2: PerlMonks Editor
created: 2006-04-17 03:22:57

Toward the top of the code is a todo list. Check item 7 in the list :).

I never use
when writing PM nodes so I guess I ignored it as a possibility. I find that between code tags and paragraphs I don't find much use for blocks of text that may be wrapped (like paragraphs), but which are single spaced (like code blocks).

For myself I prefer the current behaviour, but a configuration option (the first) could be added to control it.


DWIM is Perl's answer to Gödel
Re: PerlMonks Editor
ww
created: 2006-04-14 09:37:38
First, ++ indeed! and...

      ...You betcha' I'd use it!

I'll use it not only for my own fatfingered tab closings or braintfart formatting, but also to speed up the pace of replies, where OP is sitting online hoping one of us will respond.

Re: PerlMonks Editor
created: 2006-04-14 11:47:11

Great idea, ++! In fact, it's something I've been toying around with trying as well, I'm glad you beat me to it (I'm not much of a GUI programmer).

I do have one feature suggestion/request. Either (a)include support for entering text in non-rich forms (like Markdown), or (b)provide a plugin text-processing architecture so other people can choose to provide Markdown/WikiText/etc. support. This would ideally be combined with the option to select a "plain text mode" for entry, then render appropriately.

<-radiant.matrix->
A collection of thoughts and links from the minds of geeks
The Code that can be seen is not the true Code
I haven't found a problem yet that can't be solved by a well-placed trebuchet
Re^2: PerlMonks Editor
created: 2006-04-17 03:14:50

Sounds entierly sensible. Have you any suggestions for how? I intend to sort out file save/open as a "next thing" and I guess that is where something would plug in. A "FileLoad" object, or something less formal than that?


DWIM is Perl's answer to Gödel
Re^3: PerlMonks Editor
created: 2006-04-17 12:19:10

Shooting from the hip here...

I'd be tempted to have such things in two places. First, a config file that maps file extensions to .pm plugins (with, of course, some well-defined interface). For example, I could have .mark map to FileLoad::Plugin::Markdown, then all .mark files would be loaded/saved with that plugin.

Second, I'd have an "Convert to PM tags" option. That might scan the space ContentExport::Plugin::* to create a dialog of options. If ContentExport::Plugin::Markdown existed, for example, there would be a 'from Markdown' option. From a user perspective this would be more of a "time to publish" function.

Again, this is sort of off the top of my head. I'm willing to lend a hand in this, too, as it's a cool project in an area where I need some skill development. My time is a little thin, but I'm happy to help where I can.

<-radiant.matrix->
A collection of thoughts and links from the minds of geeks
The Code that can be seen is not the true Code
I haven't found a problem yet that can't be solved by a well-placed trebuchet
Re: PerlMonks Editor
QM
created: 2006-04-14 13:06:20
Great! (Now, this node is also a candidate for nested tags.)

-QM
--
Quantum Mechanics: The dreams stuff is made of

Re^2: PerlMonks Editor
created: 2006-04-18 18:31:27

What about code tags that just automaticaly hide code that is longer than X. Then you just click download to view it. It would be a user setting of course sense nothing on this site is allowed to change from standard behaviour ;)


___________
Eric Hodges
Re: PerlMonks Editor
created: 2006-04-18 17:51:03

That is nice, because it attempts to be PerlMonks markup aware. Here's the thing I use to edit nodes locally using any editor, whether vi, Netscape Composer, or whatever.

As you can see, the default editor I have hardcoded is my local installation of Mozilla Editor (formerly aka Netscape Composer, I think).
use LWP::UserAgent;
use HTML::Entities qw( decode_entities );
use HTTP::Request::Common qw(POST);
use Getopt::Long;

use strict;
use warnings;

my $base_url = "http://perlmonks.org/";
my $edit_cmd = '"C:\\Program Files\\mozilla.org\\Mozilla\\mozilla.exe" -editor "file://%s"';
my $node_id;
my $username;
my $password;

GetOptions(
	'node_id|id=s'      => \$node_id,
	'username=s'        => \$username,
	'password|pw=s'     => \$password,
	'editor|edit_cmd=s' => \$edit_cmd,
);

$edit_cmd =~ /%s/ or $edit_cmd .= ' "%s"';

	sub prompt_for
	{
		my $p = shift;
		print "\n$p: ";
		local $_ = <>;
		chomp;
		$_ =~ /^$/ and die "aborted.\n";
		$_
	}

$node_id  ||= prompt_for('NodeID');
$username ||= prompt_for('UserName');
$password ||= prompt_for('Password');

my %params = (
	op          => 'login',
	user        => $username,
	ticker      => 'yes',
	displaytype => 'xml',
	xmlstyle    => 'flat',
	node_id     => $node_id,
);

my $ua = LWP::UserAgent->new;
$ua->agent("NodeEditor/0.1 ");
my $params = join '&', map { $_ . '=' . $params{$_} } keys %params;
my $req = HTTP::Request->new( GET => $base_url.'?'.$params );
my $res = $ua->request($req);
$res->is_success or die "GET Error: " . $res->status_line . "\n";
$_ = $res->content;
my( $title ) = /]*>(.*)<\/doctext>/s;
$text = decode_entities( $text );
my $text_has_dos_eoln = $text =~ /\r\n/ && $text !~ /[^\r]\n/;
$text =~ s/\r//g;
my $filename = "node_$node_id.html";
my $perl_filename = "$ENV{TMP}/$filename";
open F, "> $perl_filename" or die "write $perl_filename - $!\n";
print F "$title\n$text\n\n";
close F;
print "\n";
system sprintf $edit_cmd, $perl_filename;
open F, "< $perl_filename" or die "read $perl_filename - $!\n";
$_ = do { local $/;  };
close F;
s/^]*>\s*//i;
my( $new_title, $new_text ) =
/^([^<]*)<\/title><\/head><body>(.*)<\/body><\/html>/s
	or die "you screwed up the format!";
#die "title='$new_title'\n\n'$new_text'\n\n";
$text_has_dos_eoln and $new_text =~ s/\n/\r\n/g;
$req = POST $base_url, [ %params,
	sexisgood    => "update",
	note_title   => $new_title,
	note_doctext => $new_text,
	passwd       => $password,
];
$res = $ua->request($req);
$res->is_success or die "POST Error: " . $res->status_line . "\n";
print $res->status_line, "\n";
</pre>


<div class="pmsig"><div class="pmsig-170442">
We're building the house of the future together.
</div></div>






<div class="node">

    <div class="node_title">
        
        Re^2: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/461912.html">GrandFather</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-18 18:08:39
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/544218">reply</a></li>
            <li><a href="/out/id/544218">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/461912">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/544218.xml">node</a></li>
                <li><a href="/rss/461912.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/544218.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/461912.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>This looks pertinent to version 2 of the editor - scrape the node being replied to (to allow quoted material from the OP) and post the reply directly. Thanks for the reply!</p>
<div class="pmsig"><div class="pmsig-461912">
<hr>DWIM is Perl's answer to Gödel
</div></div>






<div class="node">

    <div class="node_title">
        
        Re^3: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/404507.html">Scott7477</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-19 01:11:12
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/544256">reply</a></li>
            <li><a href="/out/id/544256">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/404507">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/544256.xml">node</a></li>
                <li><a href="/rss/404507.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/544256.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/404507.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>In your code the following line</p>
<br>
<p><tt class="inline_code">use constant kParaSpace => 4;</tt></p>
<br>
<p>appears to be the setting for the gap between paragraphs when the paragraph tags are used.  </p>
<br>
<p>When I tested your editor the gap between paragraphs is rather wider than seems to be the norm in most PerlMonks posts.  Perhaps kParaSpace should be set to 2?</p>
<br>
<p>This is a minor nitpick; otherwise this is very slick.  Kudos to you...</p>
<br>
<p>Scott</p>
PS: This node was written (mostly)using your editor:)






<div class="node">

    <div class="node_title">
        
        Re^4: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/461912.html">GrandFather</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-19 01:22:55
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/544260">reply</a></li>
            <li><a href="/out/id/544260">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/461912">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/544260.xml">node</a></li>
                <li><a href="/rss/461912.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/544260.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/461912.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>Your analysis is quite correct Scott. That feature was jammed in just before I posted the code immediately before leaving on holiday over Easter so the gap hasn't been fine tuned. However I suspect it may be influenced by things such as the default font being used by the Text widget and perhpas by various system settings and display resolution.</p>
<p>A number of things such as that could be made user preferences when the code is more fully developed.</p>
<p>Glad to hear that you are using it for real BTW - Kudos to you :). (It's really a rather preliminary version.)</p>
<div class="pmsig"><div class="pmsig-461912">
<hr>DWIM is Perl's answer to Gödel
</div></div>


</div>
</div>

</div>
</div>





<div class="node">

    <div class="node_title">
        
        Re^3: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/170442.html">jdporter</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-19 12:52:22
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/544412">reply</a></li>
            <li><a href="/out/id/544412">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/170442">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/544412.xml">node</a></li>
                <li><a href="/rss/170442.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/544412.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/170442.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <blockquote><i>
scrape the node being replied to (to allow quoted material from the OP)
</i></blockquote>

<p>
Yeah, that's a good idea. My code (above) is only useful for editing extant nodes.
</p>
<!--
<p>
Now what we need is a way (a plugin?) for launching the editor directly from the browser. It could be smart:
if you own the currently displayed node, launch the editor for that node; if not, create a reply, pre-populate it with the replied-to node content quoted, and launch the editor on that.
</p>
-->

<div class="pmsig"><div class="pmsig-170442">
We're building the house of the future together.
</div></div>


</div>
</div>

</div>
</div>

</div>
</div>





<div class="node">

    <div class="node_title">
        
        Re: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/461912.html">GrandFather</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-25 03:19:43
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/545487">reply</a></li>
            <li><a href="/out/id/545487">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/461912">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/545487.xml">node</a></li>
                <li><a href="/rss/461912.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/545487.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/461912.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>Update adds file save/load and shortcut keys. Cleans up paragraph spacing a little.</p>
<readmore title="the code">
<pre class="block_code">
use strict;
use warnings;
use diagnostics;

use Tk;
#use Tk::TextUndo; #Removed due to bugs that would be far too much effort to work around
use Tk::Balloon;
use Tk::Clipboard;
use Tk::FBox;
use Clone qw(clone);

=head Todo

Dirty document handling
Manage B, F, I, P, R and U flags
Translate entities (note U flag)
Support snippet text (with formatting)
Disallow relink
Checkmarks for menus
handle tables
Add Edit menu to access find and replace etc
Figure out node name/id from selection?
LWP to get node name from id
Make menus understand exclusion flags and group exclusive entrys when in same menu
Add email sending facility

=cut

use constant kParaSpace => 6;

my $VERSION = '001.000102'; # 1.0 Alpha 2

my $currentFile = '';
my %tagTypes;       # Style tag data
my %formatFonts;    # Fonts used in style tags. Keyed by tag
my %bindings;       # Key, menu and toolbar bindings. Tag is value
my %menuItems;      # Child menu widgets keyed by menu label path
my %entities =      # Entities we need to use outside code blocks
    (
    '&', '&',
    '<', '<',
    '>', '>',
    '[', '[',
    ']', ']',
    );
my @stdFlags = (
    'B', # Block level element
    'C', # Clear all or specified tags: C or Ctag (note lower case)
    'F', # Format tag (inline element)
    'I', # Item in a list. Implies B
    'L', # Link
    'P', # Applies to whole paragraph
    'R', # Readmore text
    'S', # Single spaced text
    'U', # Untranslated - don't translate entities
    'X', # Exclude all or specified tags: X or Xtag (note lower case)
    );

while (<DATA>) {
    # Load the default configuration stuff
    chomp;
    next if ! length;
    last if /^#key /;
    next if /^#/;
    
    my ($tag, $htmlTag, $name, $flagsField, @options) = split /\s*,\s*/;

    (print "Missing entries in tag line ($.): $_"), next if ! defined $flagsField;

    # pull out flags and handle X and C special case flags
    my %flags;
    @flags{@stdFlags} = (0) x @stdFlags; # Preset flags off
    $flags{'C'} = {};
    $flags{'X'} = {};

    for (split /(?=[A-Z][a-z]*)/, $flagsField) {
        my ($flag, $value) = split /(?<=[A-Z])/, $_;
        
        print "Unhandled flag '$flag' used\n" if ! exists $flags{$flag};
        if (-1 != index 'XC', $flag) {
            $flags{$flag}{$value || 'ALL'} = 1;
            $flags{'C'}{$value || 'ALL'} = 1 if $flag eq 'X'; # X implies C
        } else {
            $flags{$flag} = $value || 1;
            $flags{'B'} = $value || 1 if $flag eq 'I';
        }
    }

    #Fix up options
    my $optionStr = join ', ', @options;
    my %optionHash;
    
    while ($optionStr =~ /\G,?\s*((?:(?!=>).)*)=>\s*(\[[^\]]*\]|[^,]*),?\s*/g) {
        my ($option, $value) = ($1, $2);
        
        trim (\$option, \$value);
        
        if ($value =~ s/\[|\]//g) {
            # Nested options. Turn them into a hash
            my @options = split ',', $value;
            my %optionHash;
            
            for (@options) {
                my ($suboption, $subvalue) = split /\s*=>\s*/;
                
                last if ! defined $subvalue;
                trim (\$suboption, \$subvalue);
                $optionHash{$suboption} = $subvalue;
            }
            
            $value = \%optionHash;
        }
        
        $optionHash{$option} = $value;
    }
    
    $tagTypes{$tag} = [$htmlTag, $name, \%flags, \%optionHash];
}

while (<DATA>) {
    # Load key binding information
    next if /^#/;
    chomp;
    next if ! length;
    
    my ($tag, $key, $menuItem, $toolbarItem, $rightClickItem) = split /\s*,\s*/;
    (print "Missing tag in binding line ($.): $_"), next if ! defined $tag;
    $bindings{$tag} = ["$key", $menuItem, $toolbarItem, $rightClickItem];
}

my $mw = MainWindow->new (-title => "PerlMonks node editor");
my $text = $mw->Scrolled
    ('Text', -font => 'normal', -wrap => 'word', -scrollbars => 'e',);

my $status = $mw->Label(-width => 60, -relief => "sunken", -bd => 1, -anchor => 'w');
my $balloon = $mw->Balloon(-statusbar => $status);
my $msg = '';
my $balloonCharIndex = '';
my $balloonLastIndex = '';

$status->pack(-side => "bottom", -fill => "both", -padx => 2, -pady => 1);
#$balloon->attach
#    (
#    $text, -msg => \$msg,
#    -balloonposition => 'mouse',  # Not really used since the postcommand returns the real position.
#    -postcommand => \&balloonPostCommand,
#    -motioncommand => \&balloonMotionCommand,
#    );

my $menuBar = $mw->Menu (-type => 'menubar');

$mw->configure(-menu => $menuBar);
$text->pack (-expand => 'yes', -fill => 'both');

# Build file menu
$menuItems{'~File'} = $menuBar->cascade(-label => '~File', -tearoff => 0);
$menuItems{'~File'}->command (-label => '~Render', -command => \&fileRender);
$menuItems{'~File'}->command (-label => '~Open...', -command => \&fileOpen);
$menuItems{'~File'}->command (-label => '~Save', -command => \&fileSave);
$menuItems{'~File'}->command (-label => 'Save ~As...', -command => \&fileSaveAs);
$menuItems{'~File'}->command (-label => 'E~xit', -command => \&fileExit);

# Build menus and bind keys
for my $tag (keys %bindings) {
    my $menuPath = $bindings{$tag}[1];

    next if ! defined $menuPath;

    my ($top, $item) = split '/', $menuPath;
    
    next if ! defined $item;
    if (! defined $menuItems{$top}) {
        $menuItems{$top} = $menuBar->cascade(-label => $top, -tearoff => 0);
    }

    my $newItem = $menuItems{$top}->command
        (-label => $item, -command => [\&doCommand, $tag]);

    if (defined $bindings{$tag}[0]) {
        #Set up accelerator bindings
        my $key = $bindings{$tag}[0];

        next if ! length $key;
        $text->bind ("<$key>" => [\&keyCommand, $tag]);

        $key =~ s/^Control/ctrl/;
        $newItem->configure (-accelerator => $key);
    }
}

my $realText = $text->Subwidget ('scrolled');

$realText->bindtags ([$realText, ref($realText), $realText->toplevel, 'all']);
$text->bind("<Return>", \&handleReturn);
$text->bind ('<Control i>', [\&keyCommand, 'italic']);

$menuItems{'~Help'} = $menuBar->cascade(-label => '~Help', -tearoff => 0);
$menuItems{'~Help'}->command (-label => '~PerlMonks Editor Help', -command => \&help);
$menuItems{'~Help'}->command (-label => '~About', -command => \&about);

# A couple of phantom paragraph spacing tags to ease calculating paragraph spacing
$text->tagConfigure("!para_start", -spacing1 => 0, -spacing3 => -(kParaSpace));
$text->tagConfigure("!para_end", -spacing1 => -(kParaSpace), -spacing3 => 0);

MainLoop ();

sub balloonPostCommand {
    return 0 if ! length $balloonCharIndex;
    
    my %balloonCharTags;
    my  $charIndex = $text->index ("$balloonCharIndex +1 char");
    
    @balloonCharTags{$text->tagNames()} = ($balloonCharIndex);
    
    # If no tags under mouse don't post the balloon.
    return 0 if ! %balloonCharTags;
    
    if (exists $balloonCharTags{name}) {
        my ($start, $end) = $text->tagPrevrange ('name', $balloonCharIndex);
        my $name = $text->get($start, $end);
        
        $name =~ s/\|.*//;
        $msg = "link to [${name}]'s home node";
    } elsif (exists $balloonCharTags{node}) {
        my ($start, $end) = $text->tagPrevrange ('node', $balloonCharIndex);
        my $node = $text->get($start, $end);
        
        $node =~ s/\|.*//;
        $msg = "link to node id $node";
        $msg .= ' (badly formed - digits only allowed)' if $node !~ /^\d+$/;
    } else {
        return 0;
    }
    
    my @p = $text->bbox($balloonCharIndex);
    my $x = $text->rootx + $p[0] + $p[2] - 4;
    my $y = $text->rooty + $p[1] + $p[3] + 2;
    print "-$x,$y-\n";
    return "$x,$y";
}

sub balloonMotionCommand {
    my $x = $text->pointerx - $text->rootx;
    my $y = $text->pointery - $text->rooty;
    
    $balloonCharIndex = $text->index ("\@$x,$y");

    # If the same char don't cancel the balloon.
    return 0 if $balloonLastIndex eq $balloonCharIndex;
    
    # New char under mouse - cancel it so a new balloon will be posted.
    $balloonLastIndex = $balloonCharIndex;
    print ">$balloonLastIndex<\n";
    return 1;
}

sub fileRender {
    $text->clipboardClear ();
    $text->clipboardAppend (render ());
}

sub fileOpen {
    $currentFile = $text->FBox(-type => 'open', -filter => '*.pmEdit')->Show;
    
    return if ! defined $currentFile;
    
    if (! open inFile, '<', $currentFile) {
        $text->messageBox
            (
            -title => 'Load failed', -icon => 'error',
            -type => 'Ok',
            -message => "Unable to open '$currentFile' - $!"
            );
        return;
    }
    
    my @oldTags = $text->tagNames ();
    $text->delete ('1.0', 'end -1 char');
    $text->tagDelete (@oldTags);

    my @tagStates;
    my $currLine = 1;
    
    while (<inFile>) {
        next if ! /-(\S+)\s([^-]+)-(.*)/;
        my ($type, $index, $item) = ($1, $2, $3);
        
        if ($type eq 'tagon') {
            push @tagStates, [$type, $index, $item] if $item !~ /^(?:!|_)/;
        } elsif ($type eq 'tagoff') {
            push @tagStates, [$type, $index, $item] if $item !~ /^(?:!|_)/;
        } elsif ($type eq 'text') {
            if ($currLine != int ($index)) {
                $currLine = int ($index);
                $text->insert ('end', "\n");
            }
            
            $text->insert ($index, $item);
        } else {
            print "Token type $type at $index not handled.\n";
        }
    }
    
    close inFile;

    my @activeList;
    my $lastIndex = '1.0';
    
    for my $this (@tagStates) {
        my ($type, $index, $item) = @$this;
        
        if (@activeList) {
            my @tagList = buildTag (@activeList);
            
            $text->tagAdd ($_, $lastIndex, $index) for @tagList;
            $lastIndex = $index;
        }
        
        if ($type eq 'tagon') {
            push @activeList, $item;
            $lastIndex = $index;
        } else {
            @activeList = grep {$_ ne $item} @activeList;
        }
    }
    
    fixParaSpacing ();
}

sub fileSave {
    if (defined $currentFile and length $currentFile) {
        doSave ($currentFile);
    } else {
        fileSaveAs ();
    }
}

sub fileSaveAs {
    my $filename = $text->FBox(-type => 'save', -filter => '*.pmEdit')->Show;
    doSave ($filename);
}

sub doSave {
    my $filename = shift;
    
    return if ! defined $filename or ! length $filename;
    
    open outFile, '>', $filename or
        $text->messageBox
            (
            -title => 'Save failed', -icon => 'error',
            -type => 'Ok',
            -message => "Unable to create '$filename' - $!"
            );
    my @dumpText = $text->dump ('-tag', '-text', '1.0', 'end');
    my ($html, $name, $mode, $params);
    
    while (@dumpText) {
        my ($type, $item, $index) = splice @dumpText, 0, 3;
        next if $type =~ /^tago(?:n|ff)$/ and $item =~ /^(?:_|!)/;
        print outFile "-$type $index-$item\n";
    }
    
    close outFile;
    $currentFile = $filename;
}

sub fileExit {
    exit 1;
}

sub render {
    my $result;
    my $paragraph;
    my $inCode = 0;
    my @dumpText = $text->dump ('-tag', '-text', '1.0', 'end');
    my ($html, $name, $mode, $params);
    
    while (@dumpText) {
        my ($type, $item, $index) = splice @dumpText, 0, 3;
        
        next if $item =~ m'^(?:sel|para)';
        
        my $segEnd = exists $dumpText[2] ? $dumpText[2] : 'end';

        if ($type =~ /^tago(?:n|ff)$/) {
            next if $item =~ /^(?:_|!)/;
            ($html, $name, $mode, $params) = @{$tagTypes{$item}};
        }
            
        if ($type eq 'tagon') {
            if ($mode->{'L'}) {
                my ($linkCode) = $html =~ /^\S*\s*(.*)/;
                $paragraph .= "[$linkCode";
                next;
            }

            $inCode = 1 if $item eq 'code';
            $paragraph .= "<$tagTypes{$item}[0]>";
        } elsif ($type eq 'tagoff') {
            if ($mode->{'L'}) {
                $paragraph .= ']';
                next;
            }
            
            $paragraph .= "</$tagTypes{$item}[0]>";
            if ($item eq 'code') {
                $inCode = 0;
            } else {
            }
        } elsif ($type eq 'text') {
            $paragraph .= $item;

            if ($paragraph =~ /\n/) {
                if ($inCode) {
                    $result .= $paragraph;
                } else {
                    $paragraph =~ tr/\n//d;
                    if ($paragraph eq '</pre>') {
                        $result .= $paragraph;
                        $paragraph = '';
                    }
                    
                    my $newStr = "<p>$paragraph</p>\n";
                    
                    $newStr =~ s|<p></code>|</code>\n<p>|;
                    $result .= $newStr;
                }
                
                $paragraph = '';
            }
        } else {
            print "Token type $type at $index not handled.\n";
        }
    }
    
    $result =~ s|<p></p>|<br>|g;
    $result .= "<!-- Generated using PerlMonks editor version $VERSION -->";
    return $result;
}

sub keyCommand {
    my @params = @_;
    doCommand ($params[1]);
    Tk->break;
}

sub handleReturn {
    fixParaSpacing ();
}

sub doCommand {
    my %newTag = (tag => shift);
    my @selections = $text->tagRanges('sel');
    @newTag{'name', 'html', 'flags', 'params'} = @{$tagTypes{$newTag{tag}}};
        
    do {
        if (@selections) {
            my %tags;
            @tags{$text->tagNames($selections[0])} = (); # Preset current tags

            $newTag{isOn} = ! exists $tags{$newTag{tag}}; # Complement new tag's curr state
            $tags{$newTag{tag}} ||= $newTag{isOn};
            
            @newTag{'start', 'end'} = splice @selections, 0, 2;
        } else {
            my %activeTags;
            @activeTags{$text->tagNames('insert')} = ();
            return if ! exists $activeTags{$newTag{tag}};
            @newTag{'start', 'end'} = $text->tagPrevrange ($newTag{tag}, 'insert');
            $newTag{isOn} = 0;
        }
        
        return if ! defined $newTag{end};
        
        my $msg = $newTag{flags}{L} ? manageLink (%newTag) : updateTextTags (%newTag);

        if (length $msg) {
            $status->configure (-text => $msg);
            return;
        }

        
    } while (@selections);
}

sub updateTextTags {
    my %newTag = @_;
    my @dumpText = $text->dump ('-tag', '-text', $newTag{start}, $newTag{end});
    my @activeTags = $text->tagNames($newTag{start});
    my %tags;
    
    @tags{@activeTags} = (1) x @activeTags; # Preset current tags
    $tags{$newTag{tag}} = $newTag{isOn};

    TOKEN: while (@dumpText) {
        my ($type, $item, $index) = splice @dumpText, 0, 3;
        my $segEnd = exists $dumpText[2] ? $dumpText[2] : $newTag{end};

        if ($type eq 'tagon') {
            $tags{$item} = 1 if $item ne $newTag{tag};
        } elsif ($type eq 'tagoff') {
            $tags{$item} = 0 if $item ne $newTag{tag};
        } elsif ($type eq 'text') {
            my @tagList = grep {! /^_|^sel$/ && $tags{$_}} keys %tags;
            my @removeList = grep {! $tags{$_} || /^_/} keys %tags;

            # Bail if current tags preclude new tag
            for (@tagList) {
                next if ! exists $tagTypes{$_} or $newTag{tag} eq $_;
                my ($Ihtml, $Iname, $Iflags, $Iparams) = @{$tagTypes{$_}};
                
                # Check for existing tag that precludes all new tags
                if ($Iflags->{'X'}{'ALL'}) {
                    next TOKEN
                }
                
                # Check for existing tag that precludes $newTag
                if ($Iflags->{'X'}{$newTag{tag}}) {
                    next TOKEN;
                }
            }

            if ($newTag{isOn}) {
                if ($newTag{flags}->{'C'}{'ALL'}) {
                    # Strip all other tags
                    push @removeList, @tagList;
                } elsif (%{$newTag{flags}->{'C'}}) {
                    # Clear specific tags
                    push @removeList, keys %{$newTag{flags}->{'C'}};
                }
                push @tagList, $newTag{tag};
            }

            $text->tagRemove ($_, $index, $segEnd) for @removeList;
            
            @tagList = buildTag (@tagList);
            $text->tagAdd ($_, $index, $segEnd) for @tagList;
            fixParaSpacing ($index);
        } else {
            print "Token type $type at $index not handled.\n";
        }
    }
    
    return '';
}

sub manageLink {
    my %newTag = @_;
    my @activeTags = $text->tagNames($newTag{start});
    my %tags;
    
    if (! $newTag{isOn}) {
        # Remove the link
        $text->tagRemove ($newTag{tag}, $newTag{start}, $newTag{end});
        updateTextTags (%newTag);
        return '';
    }
    
    @tags{@activeTags} = (1) x @activeTags; # Preset current tags
    for (keys %tags) {
        next if ! exists $tagTypes{$_};
        return 1 if $newTag{tag} eq $_ and $newTag{isOn}; # Link already
        
        my ($Ihtml, $Iname, $Iflags, $Iparams) = @{$tagTypes{$_}};
        return "Can't link inside $Iname" if $Iflags->{'X'}{'ALL'};
        return "Can't link inside $Iname" if $Iflags->{'X'}{'link'};
    }
    
    return 'Links must not span line ends.'
        if int ($newTag{start}) != int ($newTag{end});
        
    # Get the link text
    my $orgLinkText = $text->get($newTag{start}, $newTag{end});
    my ($linkStr, $textStr) = $orgLinkText =~ /^([~|]*\|?)(.*)/;
    my $indexStr = "$newTag{start} +" . length ($linkStr) . 'chars';
    my $linkEnd = $text->index ($indexStr);
    my %linkTag = %{clone (\%newTag)};
    my %textTag = %{clone (\%newTag)};

    $linkTag{end} = $linkEnd;
    $textTag{start} = $linkEnd;
    
    updateTextTags (%linkTag);
    updateTextTags (%textTag);
    return '';
}

sub buildTag {
    my %tags;
    
    @tags{@_} = ();
    
    my @tagList = sort keys %tags;
    my $newFormatTag = '_' . join '_', @tagList;
    my %options;
    my %fontParams;
    
    for (@tagList) {
        next if ! exists $tagTypes{$_} || ! ref $tagTypes{$_};

        my ($html, $name, $mode, $params) = @{$tagTypes{$_}};
        next if ! ref $params;
        
        for my $type (keys %$params) {
            if ($type =~ /-font/) {
                for my $subType (keys %{$params->{$type}}) {
                    $fontParams{$subType} = $params->{$type}{$subType};
                }
            } else {
                $options{$type} = $params->{$type};
            }
        }
    }
    
    $options{-font} = buildFont (%fontParams) if %fontParams;
    $text->tagConfigure ($newFormatTag, %options);

    push @tagList, $newFormatTag;
    return @tagList;
}

sub buildFont {
    my %options = @_;
    my $fontName = '';
    
    $fontName .= "$_|$options{$_}," for sort keys %options;
    $fontName =~ tr/-+/mp/;
    $fontName =~ tr/a-zA-Z0-9/mp_/c;
    $mw->fontCreate($fontName, %options) if ! $formatFonts{$fontName}++;
    return $fontName;
}

sub fixParaSpacing {
    my $targetLine = shift;
    
    if (! defined $targetLine) {
        fixGlobalParaSpacing ();
        return;
    }
}

sub fixGlobalParaSpacing {
    my $lastLine = ($text->index ('end') =~ /(\d+)/)[0];
    my $lastTailSpace = -(kParaSpace);
    my @paraTags;
    
    push @paraTags, "!para_$_" for (1..$lastLine);
    $text->tagDelete (@paraTags); # Clear current spacing tags
    
    for my $line (1..$lastLine) {
        my $headSpace = kParaSpace;
        my $tailSpace = kParaSpace;
        my @activeTags = $text->tagNames("$line.0");
        
        # Note that this is currently broken if the first character happens to be a
        # part of a single spaced style applied to a partial line
        for (@activeTags) {
            next if ! exists $tagTypes{$_} || ! ref $tagTypes{$_};
    
            my ($html, $name, $mode, $params) = @{$tagTypes{$_}};
            next if ! ref $params;
    
            for my $type (keys %$params) {
                $headSpace = $params->{$type} if $headSpace && $type =~ /-spacing1/;
                $tailSpace = $params->{$type} if $tailSpace && $type =~ /-spacing3/;
            }
        }

        if ($lastTailSpace == -(kParaSpace)) {
            $headSpace = 0;
        } elsif ($lastTailSpace == 0 && $headSpace > 0) {
            $headSpace += kParaSpace;
        } elsif ($lastTailSpace > 0 && $headSpace == 0) {
            $headSpace += kParaSpace;
        }

        $text->tagConfigure("!para_$line", -spacing1 => $headSpace, -spacing3 => $tailSpace);
        $text->tagAdd ("!para_$line", "$line.0");
        $text->tagRaise ("!para_$line");
        $lastTailSpace = $tailSpace;
    }
}

sub trim {
    for (@_) {
        $$_ =~ s/^\s+//;
        $$_ =~ s/\s+$//;
    }
}

sub help {
    my $msg = <<MSG;
This editor is designed to provide wysiwyg editing for PerlMonks.org nodes. The
contents of the node is edited off-line and rendered (File|Render) to the
clipboard for pasting into a node's text edit field.

Feedback can be /msged to GrandFather in the first instance. If you provide an
email address in your /msg, GrandFather will most likely reply to the email
address.
MSG

    $mw->messageBox (
        -icon => 'info',
        -message => $msg, -title => 'PerlMonks Editor Help',
        -type => 'Ok',
        );
}

sub about {
    my $msg = <<MSG;
PerlMonks Editor

Written by GrandFather for the assistance, pleasure and edification of other
monks.
MSG

    $mw->messageBox (
        -icon => 'info',
        -message => $msg, -title => 'About PerlMonks Editor',
        -type => 'Ok',
        );
}

__DATA__
#tag style definitions
#tag name,HTML tag, UI text, flags, modifiers as key value pairs
big,big,Big font,F,-font => [-size => 16]
bold,b,Bold,F,-font => [-weight => bold]
center,center,Centered text,P,
code,code,Code block,BFXCU,-spacing1 => 0,-spacing3 => 0,-background => #e0e0ff,-font => [-family => courier, -weight => bold]
cpan,link id://,CPAN link,L, -background => #c0c0c0, -foreground => #40e040,
dd,dd,Definition Description,B,
del,del,Deleted Text,F,
dl,dl,Definition List,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m
dt,dt,Definition Term,B,-lmargin1 => 10m, -lmargin2 => 10m, -rmargin => 10m, -font => [-weight => bold]
emphasis,em,Emphasis,F,-font => [-weight => bold]
h3,h3,Header level 3,B,-font => [-size => 24], -background => #c0c0c0,-spacing1 => 18
h4,h4,Header level 4,B,-font => [-size => 24], -background => #8080c0,-spacing1 => 14
h5,h5,Header level 5,B,-font => [-size => 16], -background => #c0c0c0,-spacing1 => 14
h6,h6,Header level 6,B,-font => [-size => 16], -background => #8080c0,-spacing1 => 10
hrule,hr,Horizontal rule,BX,
inserted,ins,ins,BF, -background => #ffffc0,
italic,i,Italic,F,-font => [-slant => italic]
item,li,List item,I,
olist,ol,Ordered list,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m
quote,blockquote,Quoted block,P,-lmargin1 => 15m,-lmargin2 => 15m,-rmargin => 15m
readmore,readmore,Read more block,BR,-background => #a0b7ce
small,small,small,F,-font => [-size => 8]
spoiler,spoiler,Spoiler,F, -background => #000000, -foreground => #404040,
strike,strike,Strike Out,F,-overstrike => on
strong,strong,Strong emphasis,F,
sub,sub,Sub script,FCsuper,-offset => -2p,-font => [-size => 8]
super,sup,Super script,FCsub,-offset => 4p,-font => [-size => 8]
teletype,Teletype text,tt,F,-font => [-family => courier], -background => #FFFFc0
ulist,ul,Unordered list,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m
underline,u,Underline,F,[-underline => on]],

#links - still tag style definitions
acronym,link acronym://,Acronym link,L, -background => #f0f0f0, -foreground => #0060c0,
cpan,link cpan://,Cpan link,L, -background => #f0f0f0, -foreground => #00a0a0,
dict,link dict://,Dictionary link,L, -background => #f0f0f0, -foreground => #00a0a0,
dist,link dist://,CPAN Distro link,L, -background => #f0f0f0, -foreground => #00a0a0,
doc,link doc://,perldoc link,L, -background => #f0f0f0, -foreground => #00a0a0,
ftp,link ftp://,Ftp link,L, -background => #f0f0f0, -foreground => #00a0a0,
google,link google://,Google link,L, -background => #f0f0f0, -foreground => #00a0a0,
href,link href://,Href link,L, -background => #f0f0f0, -foreground => #00a0a0,
http,link http://,Http link,L, -background => #f0f0f0, -foreground => #00a0a0,
https,link https://,Https link,L, -background => #f0f0f0, -foreground => #00a0a0,
id,link id://,Node id link,L, -background => #f0f0f0, -foreground => #00a0a0,
isbn,link isbn://,Isbn link,L, -background => #f0f0f0, -foreground => #00a0a0,
jargon,link jargon://,Jargon link,L, -background => #f0f0f0, -foreground => #00a0a0,
kobes,link kobes://,Kobes link,L, -background => #f0f0f0, -foreground => #00a0a0,
lj,link lj://,Live journal link,L, -background => #f0f0f0, -foreground => #00a0a0,
lucky,link lucky://,Google lucky link,L, -background => #f0f0f0, -foreground => #00a0a0,
mod,link mod://,Mod link,L, -background => #f0f0f0, -foreground => #00a0a0,
module,link module://,Module link,L, -background => #f0f0f0, -foreground => #00a0a0,
name,link,Node name link,L, -background => #f0f0f0, -foreground => #0060c0,
pad,link pad://,Scratchpad link,L, -background => #f0f0f0, -foreground => #00a0a0,
perldoc,link perldoc://,Perldoc link,L, -background => #f0f0f0, -foreground => #00a0a0,
pmdev,link pmdev://,Pmdev link,L, -background => #f0f0f0, -foreground => #00a0a0,
wp,link wp://,Wp link,L, -background => #f0f0f0, -foreground => #00a0a0,

#key bindings, menu items and tool bar items
#tag,key,menu item,toolbar item,right click item
big,Control 2,Format/Big,,Big
bold,Control b,Format/Bold,,Bold
cpan,,Links/CPAN,,CPAN link
italic,Control i,Format/Italic,,Italic
strike,Control s,Format/Strike out,,Strike out
sub,Control u,Format/Subscript,,Subscript
super,Control s,Format/Superscript,,Superscript
code,Control k,Format/Code,,Code
id,Control d,Links/Node,,Node id link
name,Control n,Links/Name,,Node name link
</c>

<p>Update: fixed nasty hack used to retreive the Text Subwidget.<br/>
Fixed file open error if you cancel out of the Open file dialog.</p>
<div class="pmsig"><div class="pmsig-461912">
<hr>DWIM is Perl's answer to Gödel
</div></div>






<div class="node">

    <div class="node_title">
        
        Re^2: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/277846.html">Moriarty</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-04-30 07:08:58
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/546575">reply</a></li>
            <li><a href="/out/id/546575">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/277846">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/546575.xml">node</a></li>
                <li><a href="/rss/277846.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/546575.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/277846.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>After much prodding by <a href="/out/node/GrandFather">GrandFather</a>, I finally got around to trying this out, on <a href="/out/http/?url=www.javajunkies.org%2Findex.pl">JavaJunkies</a>. I can happily report that it works quite well, even with numbered links, although I don't know for how much longer as <a href="/out/node/Yendor">Yendor</a> keeps promising a Java front end for that site.</p>
<!-- Generated using PerlMonks editor version 001.000102 -->


</div>
</div>

</div>
</div>





<div class="node">

    <div class="node_title">
        
        Re: PerlMonks Editor
                
    </div>

    <div class="node_author">
        <a href="/html/461912.html">GrandFather</a>
    </div>

    <div class="node_extra">

    <div class="node_details">
        created: 2006-05-04 22:46:50
    </div>

    <div class="node_links">

        <div class="node_link_block">
            <strong>perlmonks</strong>
            
            <ul>
            <li><a href="/out/comment/547578">reply</a></li>
            <li><a href="/out/id/547578">node</a></li>
            <li><a href="/out/id/543242">thread</a></li>
            <li><a href="/out/id/461912">author</a></li>
            </ul>        
        </div>   


        <div class="node_link_block">
            <img src="/decor/small-rss.png" />
            
            <ul>
                <li><a href="/rss/547578.xml">node</a></li>
                <li><a href="/rss/461912.xml">author</a></li>
                <li><a href="/rss/543242.xml">thread</a></li>
            </ul>        
        </div>   

        <div class="node_link_block">
            <strong>prlmnks</strong>
    
            <ul>
                <li><a href="/html/547578.html">node</a></li>
                <li><a href="/html/543242.html">thread</a></li>
                <li><a href="/html/461912.html">author</a></li>
            </ul>        
        </div>   

    </div>
    </div>

    <div class="node_content">

        <p>Further development of this code has moved to CPAN. THe current version (as of writing this node) is <a href="/out/http/?url=cpan.perl.org%2Fauthors%2Fid%2FG%2FGR%2FGRANDPA%2FPMEdit%2FPMEdit-001.000104-1.pl">here</a> which is accessed from <a href="/out/http/?url=cpan.perl.org%2Fscripts%2F">http://cpan.perl.org/scripts/</a> or <a href="/out/http/?url=cpan.perl.org%2Fscripts%2FWeb%2Findex.html">http://cpan.perl.org/scripts/Web/index.html</a>.</p>
<div class="pmsig"><div class="pmsig-461912">
<hr>DWIM is Perl's answer to Gödel
</div></div>


</div>
</div>

</div>
</div>









<div id='bottom'>

    <p><a href="http://perlmonks.org">perlmonks.org</a> content © perlmonks.org and eric256, GrandFather, jdporter, McDarren, Moriarty, QM, radiantmatrix, Scott7477, VSarkiss, ww
    </p>

    <p>
        <a href="http://prlmnks.org">prlmnks.org</a> © 2006 
        <a href="mailto:evdb@ecclestoad.co.uk">edmund von der burg</a>
        (<a href="http://ecclestoad.co.uk">eccles & toad</a>)
        
    </p>
    
    <p>v 0.03</p>

</div>

<div id="bug">
    <script language="JavaScript" type="text/javascript">
        document.write('<a href="http://dev.ecclestoad.co.uk/trac/prlmnks/newticket?description=' + escape(window.location) + '"><img src="/decor/bug.png" alt="report a bug" border="0" title="report a bug"/></a>         ');
    </script>
</div>

</body>
</html>