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 .= "$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 ' ') {
$result .= $paragraph;
$paragraph = '';
}
$result .= "$paragraph
\n"; } $paragraph = ''; } } else { print "Token type $type at $index not handled.\n"; } } $result =~ s||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
# Where as: Blah blah some text and random pontification that goes on for a few lines. Then a new paragraph starts here... # Converts to:
And another line directly underneathBlah blah some text and random pontification that goes on for a few lines.
Then a new paragraph starts here...
Cheers,
Darren :)
...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?
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.
...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.
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.
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?
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.
-QM
--
Quantum Mechanics: The dreams stuff is made of
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 ;)
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><\/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";
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!
In your code the following line
use constant kParaSpace => 4;
appears to be the setting for the gap between paragraphs when the paragraph tags are used.
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?
This is a minor nitpick; otherwise this is very slick. Kudos to you...
Scott
PS: This node was written (mostly)using your editor:)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.
A number of things such as that could be made user preferences when the code is more fully developed.
Glad to hear that you are using it for real BTW - Kudos to you :). (It's really a rather preliminary version.)
scrape the node being replied to (to allow quoted material from the OP)
Yeah, that's a good idea. My code (above) is only useful for editing extant nodes.
Update adds file save/load and shortcut keys. Cleans up paragraph spacing a little.
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 () {
# 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];
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("", \&handleReturn);
$text->bind ('', [\&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 () {
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 ' ') {
$result .= $paragraph;
$paragraph = '';
}
my $newStr = "$paragraph
\n"; $newStr =~ s||\n
|; $result .= $newStr; } $paragraph = ''; } } else { print "Token type $type at $index not handled.\n"; } } $result =~ s|
|Update: fixed nasty hack used to retreive the Text Subwidget.
Fixed file open error if you cancel out of the Open file dialog.
After much prodding by GrandFather, I finally got around to trying this out, on JavaJunkies. I can happily report that it works quite well, even with numbered links, although I don't know for how much longer as Yendor keeps promising a Java front end for that site.
Further development of this code has moved to CPAN. THe current version (as of writing this node) is here which is accessed from http://cpan.perl.org/scripts/ or http://cpan.perl.org/scripts/Web/index.html.
perlmonks.org content © perlmonks.org and eric256, GrandFather, jdporter, McDarren, Moriarty, QM, radiantmatrix, Scott7477, VSarkiss, ww
prlmnks.org © 2006 edmund von der burg (eccles & toad)
v 0.03