#!/usr/bin/perl use strict; use warnings; BEGIN {our @usedModules; unshift @INC, sub {push @usedModules, [@_]; return undef;}} our @usedModules; use Tk; #use Tk::TextUndo; #Removed due to bugs that are too much effort to work around ATM use Tk::Balloon; use Tk::Clipboard; use Storable qw(dclone); my $VERSION = '001.000203'; # 1.0 Beta 3 use constant DEBUG => 0; use constant kParaSpace => 6; =head1 NAME PMEdit - A wysiwyg PerlMonks.org markup savy editor. =head1 DESCRIPTION This script is a PerlMonks.org markup savy editor. It may be useful for most everything based web sites and can be adapted for use for light weight HTML generation. =head1 README PerlMonks editor is designed to allow wysiwig editing of material to be posted on everything based web sites such as PerlMonks. Initial versions are intended to be used to prepare the material offline and then render to the clipboard for pasting into a node's edit field. It is expected that later versions will interact more directly with the web site to allow easier updating of existing nodes and quoting material from nodes that are being replied to. The current version is considered to be a beta version. It does some cool stuff and the main intial features are implemented with a good number of bugs ironed out. There are still various editing foibles due to the way the Tk Text widget behaves that may get resolved before the notional final version 1 release, but more likely won't. The current version provides configuration information for associating markup with display styles, menu entries, key assignements and (in the future) toolbar entries. The configuration is included in the script in a __DATA__ section. There are two sectons in the configuration data seperated by a line starting with "#key ". The first section contains information mapping tags to display formatting and management and output rendering. The lines are of the form: tag name,HTML tag, UI text, flags, modifiers as key value pairs For example code,c,Code block,BFXCU,-spacing1 => 0,-spacing3 => 0,-foreground => #e0e0ff,-font => [-family => courier, -weight => bold] =over 8 code: the name used internally for tagging text c: the HTML or link element tag text Code block: String that may be used in the user interface BFXCU: flags that control display, placement and rendering ...: display formatting. See the Tk::Text TAGS section =back The following flags may be used: =over 8 B: Block level element (paragraph tag

) C: Clear all or specified tags: C or Ctag (note lower case). Allows code tags to reset any other tags when the code tag format is applied for example. F: Format tag (inline element such as bold ) I: Item in a list. Implies B. Will get special display handling. L: Link. Gets [] brackets to signal a PerlMonks link N: Needs block level tag (any one of multiple): Ntag. Used to ensure a the flaged tag is a child element of one of the specified element types. For example a list item (I flag) would specify NolistNulist to indicate it must be contained in a ol or ul element. P: Applies to whole paragraph. Set for element types such as and R: Readmore text. semantics (implies P) S: Single spaced text. Prevents additional paragraph spacing on the displayed text (doesn't affect output rendering). U: Untranslated - don't translate entities. Used in code elements and other elements to retain characters such as <>&[] as litteral characters. X: Exclude all or specified tags: X or Xtag (note lower case). Prevent the listed tags being applied in regions that contain the current tag. Used to prevent formatting being applied in a code block for example. =back The second section describes key and menu bindings for tags. Eventually toolbar support may be added also. The lines in this section are of the form: tag,key,menu item,toolbar item,right click item For example: code,Control k,Format/Code,,Code =over 4 code: the tag name used in the previous section Control k: the key combination used to access the tag Format/Code: the menu path to the entry used to access the tag. In this case a 'Code' entry would be created in the 'Format' menu missing: The missing entry is a place holder for a toolbar entry Code: the right click menu entry used to access the tag =back a special case entry is used to put dividers in menus. It is of the form: -,-,Format/-,, Note that menu entries are currently generated in the order that they are specified in the configuration section. =head1 PREREQUISITES This script requires the following modules: C C C C C C =pod OSNAMES any =head1 AUTHOR Peter Jaquiery > =head1 COPYRIGHT Copyright (c) 2006, Peter Jaquiery. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =pod SCRIPT CATEGORIES Web =cut our @bindings; # Key, menu and toolbar bindings. our @stdFlags; our %tagTypes; my $currentFile = ''; my %formatFonts; # Fonts used in style tags. Keyed by tag my %menuItems; # Child menu widgets keyed by menu label path my %entities = # Entities we need to use outside code blocks ( '&', '&', '<', '<', '>', '>', '[', '[', ']', ']', ); my @filetypes = ( ['PerlMonks editor', '.PMEdit', 'TEXT'], ['PerlMonks editor test', '.PMEditT', 'TEXT'], ['PerlMonks editor test', '.PMEditF', 'TEXT'], ['Text', '.txt', 'TEXT'], ); LoadConfig (); 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 => '~New', -command => \&fileNew); $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 my %commands = ( selAll => \&do_selAll, copy => \&do_copy, paste => \&do_paste, ); my $realText = $text->Subwidget ('scrolled'); for my $tagData (@bindings) { my $menuPath = $tagData->[2]; next if ! defined $menuPath; my $tag = $tagData->[0]; my ($top, $item) = split '/', $menuPath; next if ! defined $item; # Not allowed in top level menus if (! defined $menuItems{$top}) { $menuItems{$top} = $menuBar->cascade(-label => $top, -tearoff => 0); } my $newItem; if ($tag eq '-') { $newItem = $menuItems{$top}->separator (); next; } elsif (exists $commands{$tag}) { $newItem = $menuItems{$top}->command (-label => $item, -command => $commands{$tag}); } else { $newItem = $menuItems{$top}->command (-label => $item, -command => [\&doCommand, $tag]); } next if ! defined $tagData->[2]; #Set up accelerator bindings my $key = $tagData->[1]; next if ! length $key; my $ok = eval { if (exists $commands{$tag}) { $realText->bind ("<$key>" => $commands{$tag}); } else { $realText->bind ("<$key>" => [\&keyCommand, $tag]); } 1; }; if (! $ok) { print "Unable to bind <$key> to <$tag>: $@\n"; next; } $key =~ s/^Control/ctrl/; $key =~ s/^$/ctrl $1/; $newItem->configure (-accelerator => $key); } $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 (); my @dumpText = $text->dump ('-tag', '-text', '1.0', 'end'); $text->clipboardAppend (Render (\%tagTypes, @dumpText)); } sub fileOpen { $currentFile = $text->getOpenFile ( -defaultextension => '.pmEdit', -filetypes => \@filetypes ); 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 fileNew { my @oldTags = $text->tagNames (); $text->delete ('1.0', 'end -1 char'); $text->tagDelete (@oldTags); $currentFile = undef; } sub fileSave { if (defined $currentFile and length $currentFile) { doSave ($currentFile); } else { fileSaveAs (); } } sub fileSaveAs { my $filename = $text->getSaveFile (-defaultextension => '.pmEdit', -filetypes => \@filetypes); 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 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 = %{dclone (\%newTag)}; my %textTag = %{dclone (\%newTag)}; $linkTag{end} = $linkEnd; $textTag{start} = $linkEnd; updateTextTags (%linkTag); updateTextTags (%textTag); return ''; } sub do_selAll { $text->selectAll (); Tk->break (); } sub do_copy { $text->clipboardColumnCopy (); } sub do_paste { $text->clipboardPaste (); } 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 help { my $msg = <messageBox ( -icon => 'info', -message => $msg, -title => 'PerlMonks Editor Help', -type => 'Ok', ); } sub about { my $versions = ''; for (sort @usedModules) { my $name = $_->[1]; $name =~ s/\..*//; $name =~ s|[\\/]|::|g; next if $name =~ /^::/; my $version = $name->VERSION; $versions .= "$name \t$version\n" if defined $version; } my $msg = <<"MSG"; PerlMonks Editor Written by GrandFather for the assistance, pleasure and edification of other monks. Module\tVersion PMEdit\t$VERSION $versions MSG $mw->messageBox ( -icon => 'info', -message => $msg, -title => 'About PerlMonks Editor', -type => 'Ok', ); } use constant TYPE => 0; use constant VALUE => 1; use constant INDEX => 2; sub Render { my $tagTypes = shift; my $blockType; my ($html, $name, $mode, $params); my $chunk = ''; my @chunks = preprocessDump ($tagTypes, @_); my @paragraphs; my $paragraph; my %bfTags; # track block/format tag usage for (@chunks) { my ($type, $item, $index) = @$_; #next if $type =~ m/^tago(?:n|ff)$/ and $item =~ m'^(?:sel|_|!)'; # Ignore if ($type eq 'para' && defined $paragraph && length $paragraph) { # Start of new paragraph my ($endCharIndex) = $item =~ /\.(\d+)/; # Figure out if previous is a non-p block my $paraType; for my $tag (keys %bfTags) { # Check for Block/Format (code for example) tags in block mode my $on = exists $bfTags{$tag}{on} && $bfTags{$tag}{on}; my $off = exists $bfTags{$tag}{off} && $bfTags{$tag}{off}; next if $on != 1; next if $off != 1; my ($lastOffChar) = $bfTags{$tag}{lastOffAt} =~ /\.(\d+)/; my ($firstOnChar) = $bfTags{$tag}{firstOnAt} =~ /\.(\d+)/; next if $lastOffChar != $endCharIndex or $firstOnChar != 0; $paraType = $tag; } if (! defined $paraType) { $paragraph = "

$paragraph

" ; } else { # Ensure open tag is followed by a new line $paragraph =~ s|^(<$paraType>)(?!\n)|$1\n|; # Ensure close tag is preceeded by a new line $paragraph =~ s|(?)|\n$1|; } push @paragraphs, $paragraph; print "\n" if DEBUG > 1; $paragraph = ''; %bfTags = (); next; } #next unless definedValue($tagTypes, $item); if ($type eq 'tagon') { # Render on tags my ($html, $name, $mode, $params) = @{$tagTypes->{$item}}; $bfTags{$item}{on}++ if $mode->{'B'} && $mode->{'F'}; $bfTags{$item}{firstOnAt} = $index if ! exists $bfTags{$item}{firstOnAt}; if ($mode->{L}) { $html =~ s/\w+\s?//; $chunk .= "[$html"; next; } elsif ($mode->{'B'} && $index =~ /\.0$/) { $blockType = $html; } $chunk .= "<$html>"; next; } if ($type eq 'tagoff') { # Render off tags my ($html, $name, $mode, $params) = @{$tagTypes->{$item}}; $bfTags{$item}{off}++ if $mode->{'B'} && $mode->{'F'}; $bfTags{$item}{lastOffAt} = $index; if ($mode->{L}) { $chunk .= ']'; } elsif (defined $blockType && $blockType eq $html && $mode->{'B'} && $index =~ /\.0$/) { $chunk .= "\n"; $blockType = undef; } else { $chunk .= "{$item}[TYPE]>"; } next; } next if $type eq 'para'; if ($type ne 'text') { print "Token type $type at $index not handled.\n"; } $chunk .= $item if defined $item; # Add the text } continue { if (length ($chunk)) { print $chunk if DEBUG > 1; $chunk =~ s/\n\Z//; $paragraph .= $chunk; $chunk = ''; } } my $result = join "\n", @paragraphs; $result =~ s|

|
|gm; $result =~ s|\n\n\n|\n|gm; $result =~ s|\n(?!\n)|\n\n|gm; $result =~ s|(?\n|\n\n|gm; $result .= "\n"; print $result if DEBUG; return $result; } sub preprocessDump { my $tagTypes = shift; my @paragraphs; my @paragraph; my @chunks; # Pull out individual edit elements push @chunks, [splice @_, 0, 3, ()] while @_; @chunks = grep {$_->[TYPE] !~ m/^tago(?:n|ff)$/ or $_->[VALUE] !~ m/^(?:sel|_|!)/} @chunks; my $lastLineNum = 1; my $lineNum; for my $chunk (@chunks) { $chunk->[VALUE] =~ s/\n//g; ($lineNum) = $chunk->[INDEX] =~ /(\d+)/; next if $lastLineNum == $lineNum; push @paragraphs, [@paragraph]; @paragraph = (); } continue { push @paragraph, $chunk; $lastLineNum = $lineNum; } push @paragraphs, [@paragraph] if @paragraph; # Migrate 'off' tags from start of current paragraph to end of previous my $lastPara; my $lastParaEndIndex; for my $para (@paragraphs) { next if ! defined $lastPara; next if @$para < 2; # Don't move single tags for (@$para) { next if $_->[TYPE] eq 'tagon'; last if $_->[TYPE] ne 'tagoff'; push @$lastPara, splice @$para, 0, 1; $lastPara->[-1][2] = $lastParaEndIndex; } } continue { $lastPara = $para; my ($line, $offset) = $lastPara->[-1][2] =~ m/(\d+)\.(\d+)/; $offset += length ($lastPara->[-1][1]) if $lastPara->[-1][0] eq 'text'; $lastParaEndIndex = "$line.$offset"; } # Finally unpack the paragraphs and provide missing tags my %tags; @chunks = (); for my $paragraph (@paragraphs) { my $startIndex = $paragraph->[0][INDEX]; my @tagNesting; my $endIndex = int ($startIndex) . 'end'; # Ignore bogus paragraphs if (1 == @$paragraph && $paragraph->[0][TYPE] eq 'tagoff') { my $type = $paragraph->[0][VALUE]; #next unless definedValue($tagTypes, $type); my ($html, $name, $mode, $params) = @{$tagTypes->{$type}}; next if $mode->{'B'}; # Skip orphaned terminating block level tag } # Provide tagons at start of paragraph for (grep {$tags{$_}} sort keys %tags) { push @chunks, ['tagon', $_, $startIndex]; push @tagNesting, $_; } for my $chunk (@$paragraph) { $endIndex = $chunk->[INDEX]; if ($chunk->[TYPE] eq 'tagon') { $tags{$chunk->[VALUE]}++; push @tagNesting, $chunk->[VALUE]; }elsif ($chunk->[TYPE] eq 'tagoff') { # Manage correct nesting of tags by supplying off tags as required my $matchIndex; my $chunkIndex = $chunk->[INDEX]; for (reverse 0 .. $#tagNesting) { if ($tagNesting[$_] eq $chunk->[VALUE]) { $matchIndex = $_; last; } push @chunks, ['tagoff', $tagNesting[$_], $chunkIndex]; } push @chunks, $chunk; $tags{$chunk->[VALUE]}--; splice @tagNesting, $matchIndex, 1; push @chunks, ['tagon', $tagNesting[$_], $chunkIndex] for $matchIndex .. $#tagNesting; next; # Avoid pushing chunk twice } elsif ($chunk->[TYPE] eq 'text') { $endIndex =~ /(\d+)\.(\d+)/; $endIndex = "$1." . ($2 + length $chunk->[VALUE]); } push @chunks, $chunk; } # Provide tagoffs at end of paragraph for my $tag (reverse @tagNesting) { push @chunks, ['tagoff', $tag, $endIndex]; } $startIndex = int ($endIndex + 1) . '.0'; push @chunks, ['para', $endIndex, $startIndex]; } return @chunks; } sub definedValue { my ($hash , $key) = @_; return exists $hash->{$key} && defined $hash->{$key}; } BEGIN { our @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 'N', # Needs block level tag (any one of multiple): Ntag '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) ); } sub LoadConfig { my $ok = 1; while () { # Load the default configuration stuff s/^\s+//; s/\s+$//; next if ! length; last if /^#key /; next if /^#/; my ($tag, $htmlTag, $name, $flagsField, @options) = split /\s*,\s*/; if (! defined $flagsField) { print "Missing entries in tag line ($.): $_"; $ok = 0; next; } # pull out flags and handle X and C special case flags my %flags; @flags{@stdFlags} = (0) x @stdFlags; # Preset flags off $flags{'C'} = {}; $flags{'N'} = {}; $flags{'X'} = {}; for (split /(?=[A-Z][a-z]*)/, $flagsField) { my ($flag, $value) = split /(?<=[A-Z])/, $_; if (! exists $flags{$flag}) { print "Unhandled flag '$flag' used\n"; $ok = 0; } if (-1 != index 'XC', $flag) { $flags{$flag}{$value || 'ALL'} = 1; $flags{'C'}{$value || 'ALL'} = 1 if $flag eq 'X'; # X implies C } elsif ($flag eq 'N') { if (! defined $value) { print "Flag N requires a block tag - it has been ignored for $tag.\n"; } else { $flags{$flag} = $value || 1; } } 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 /^#/; s/^\s+//; s/\s+$//; next if ! length; my ($tag, $key, $menuItem, $toolbarItem, $rightClickItem) = split /\s*,\s*/; if (! defined $tag) { print "Missing tag in binding line ($.): $_"; $ok = 0; next; } push @bindings, [$tag, "$key", $menuItem, $toolbarItem, $rightClickItem]; } return $ok; } sub trim { for (@_) { $$_ =~ s/^\s+//; $$_ =~ s/\s+$//; } } __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,-justify => center code,code,Code block,BFXCU,-spacing1 => 0,-spacing3 => 0,-foreground => #8080e0,-font => [-family => courier, -weight => bold] 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 => [-slant => italic] 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,INolNul, 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,B, -background => #000000, -foreground => #404040, strike,strike,Strike Out,F,-overstrike => on strong,strong,Strong emphasis,F,-font => [-weight => bold] sub,sub,Sub script,FCsuper,-offset => -2p,-font => [-size => 8] super,sup,Super script,FCsub,-offset => 4p,-font => [-size => 8] teletype,tt,Teletype text,F,-font => [-family => courier], -background => #FFFFc0 ulist,ul,Unordered list,B,-lmargin1 => 20m, -lmargin2 => 20m, -rmargin => 20m underline,u,Underline,F,-underline => 1, #links - still tag style definitions acronym,link acronym://,Acronym link,L, -underline => 1, -foreground => #0060c0, cpan,link cpan://,Cpan link,L, -underline => 1, -foreground => #00a0a0, dict,link dict://,Dictionary link,L, -underline => 1, -foreground => #00a0a0, dist,link dist://,CPAN Distro link,L, -underline => 1, -foreground => #00a0a0, doc,link doc://,perldoc link,L, -underline => 1, -foreground => #00a0a0, ftp,link ftp://,Ftp link,L, -underline => 1, -foreground => #00a0a0, google,link google://,Google link,L, -underline => 1, -foreground => #00a0a0, href,link href://,Href link,L, -underline => 1, -foreground => #00a0a0, http,link http://,Http link,L, -underline => 1, -foreground => #00a0a0, https,link https://,Https link,L, -underline => 1, -foreground => #00a0a0, id,link id://,Node id link,L, -underline => 1, -foreground => #00a0a0, isbn,link isbn://,Isbn link,L, -underline => 1, -foreground => #00a0a0, jargon,link jargon://,Jargon link,L, -underline => 1, -foreground => #00a0a0, kobes,link kobes://,Kobes link,L, -underline => 1, -foreground => #00a0a0, lj,link lj://,Live journal link,L, -underline => 1, -foreground => #00a0a0, lucky,link lucky://,Google lucky link,L, -underline => 1, -foreground => #00a0a0, mod,link mod://,Mod link,L, -underline => 1, -foreground => #00a0a0, module,link module://,Module link,L, -underline => 1, -foreground => #00a0a0, name,link,Node name link,L, -foreground => #0060c0, -underline => 1 pad,link pad://,Scratchpad link,L, -underline => 1, -foreground => #00a0a0, perldoc,link perldoc://,Perldoc link,L, -underline => 1, -foreground => #00a0a0, pmdev,link pmdev://,Pmdev link,L, -underline => 1, -foreground => #00a0a0, readmore,readmore,Readmore,RB,-stipple => 1, wp,link wp://,Wp link,L, -underline => 1, -foreground => #00a0a0, #key bindings, menu items and tool bar items #tag,key,menu item,toolbar item,right click item selAll,Control a,Edit/Select All,,Select All copy,Control c,Edit/Copy,,Copy paste,,Edit/Paste,,Paste big,Control 2,Format/Big,,Big bold,Control b,Format/Bold,,Bold center,,Format/Center,,Center italic,Control i,Format/Italic,,Italic item,,Format/List item,,List item small,Control s,Format/Small,,Small strike,Control s,Format/Strike out,,Strike out sub,Control u,Format/Subscript,,Subscript super,Control s,Format/Superscript,,Superscript underline,Control underscore,Format/Underline,,Underline -,-,Format/-,-, code,Control k,Format/Code,,Code quote,Control q,Format/Blockquote,,Blockquote ulist,,Format/Unordered list,,Unordered list #links - still bindings cpan,,Links/CPAN,,CPAN link id,Control d,Links/Node,,Node id link name,Control n,Links/Name,,Node name link