#!/usr/bin/perl
my $VERSION = "0.4.1";
use Tk;
use Tk::ROText;
use Tk::Dialog;
use Tk::LabEntry;
use Data::Dumper;
use subs qw/file_menuitems edit_menuitems help_menuitems/;
my $ww = 80;
my $geometry = '600x450+1+1'; # Smaller than 800x600
my $fn = '6x12';
my $sliderWidth = 12;
my $progname;
{
my @path = split( m|/|, $0 );
$progname = $path[-1];
}
my $welcome = '';
$welcome .= "A program to help you build a resume/CV using the ";
$welcome .= "XML-Resume-Library. It helps in the sense of playing ";;
$welcome .= "20 questions with you to input data, edit things you've ";
$welcome .= "entered before, re-arrange the order things are printed ";
$welcome .= "in, etc. It does not provide guidance as to what should ";
$welcome .= "be in a resume or CV.\n\n";
$welcome .= "The only really required information, is the contact ";
$welcome .= "information needed so that potential employers can get ";
$welcome .= "a hold of you if they need to.\n\n";
$welcome .= "Be aware that a lot of information that could go into a ";
$welcome .= "resume or CV is sensitive. You may not want such information ";
$welcome .= "publically available. At best, it might result in yourself, ";
$welcome .= "or others mentioned in your resume, getting spammed.";
#$welcome .= "";
# Do any cleanup needed before exit.
sub myexit {
return 1;
}
# This is entirely tooooo manual. It would be nice if some kind of
# templating could be done from DTD or from (edited) schema produced
# by having dtd2xsd.pl process the DTD. (Edited to remove ambiguity
# this is necessary for DTD, but not required for Schema.)
# Hardwire that this programs runs in GUI mode.
$gui = 1;
# Copy of active (in RAM) resume data.
my $cache = {};
# In RAM copy of database (maybe).
my $dbase = {};
my(%sections, @sections, %options, @options, %fields, @fields);
my(%Sections, @Sections, %Options, @Options, %Fields, @Fields);
if( $gui ) {
&guiMain();
&myexit;
exit 0;
}
exit 0;
# Effective end of main.
my( $mw );
my( $mainFrame );
sub guiMain {
$ui = 'gui';
# When we create MainWindow ($mw), it implicitly calls
# Tk::CmdLine::SetArguments to strip/process any command
# line arguments dealing with X11 display and geometry.
# We might want to call Tk::CmdLine::LoadResources to get
# stuff from a file.
$mw = MainWindow->new();
# We could define an icon for minimization with
# $mw->iconimage( $mw->Pixmap(-file=>"path/to/icon.xpm") );
# A whole bunch of stuff is implicitly defined by making the
# MainWindow. We might want to optionGet() stuff to either
# remember it, or tune it.
# Set up MainWindow
$mw->wm('title', 'XMLResume-Tk');
$mw->wm('iconname', 'XMLResume-Tk');
$mw->wm('geometry', $geometry);
# Set up menus
$mw->configure(-menu=> my $menubar = $mw->Menu );
$mw_menu{File} = $menubar->cascade( -label => '~File',
-menuitems => file_menuitems );
$mw_menu{Edit} = $menubar->cascade( -label => '~Edit',
-menuitems => edit_menuitems );
$mw_menu{Help} = $menubar->cascade( -label => '~Help',
-menuitems => help_menuitems );
# Show a banner window with a welcome message and a Quit button.
$mw->Label( -text => "$progname", -font => "9x15" )->pack();
$mw_text1 = $mw->ROText(-wrap=>'word',
-relief=>'flat',
-font=>'9x15')->pack();
$mw_text1->insert( 'end', $welcome );
$mw->Button(-text=>'Quit', -command=>sub { $mw->destroy(); } )->pack();
# As with all GUIs, go into an endless event loop
MainLoop;
# When we drop out of loop, we hit here.
print "All done\n";
}
# ====================== Menus ========================
sub file_menuitems {
[
[qw/command ~New -command/ => \&GUI_New_DBase],
[qw/cascade ~Save -menuitems/ =>
[
[qw/command ~Dumper -command/ => sub { &GUI_Save('Dumper'); }],
[qw/command ~Storable -command/ => sub { &GUI_Save('Storable'); }],
]
],
[qw/command ~Quit -command/ => sub {$mw->destroy(); }],
];
}
sub edit_menuitems {
[
['command', 'Add', -command=>\&GUI_Add_Entry],
['command', 'Order', -command=>\&GUI_Display_Order],
];
}
sub help_menuitems {
[
['command', 'Version', -command=> sub { print "Version\n"; }],
];
}
sub GUI_New_DBase {
print "Not yet, sorry\n";
}
sub GUI_Save {
my $type = shift;
}
sub GUI_Add_Entry {
my $default_button = 'Quit';
# Build an array of Buttons for the user, properly capitalized.
my @buttons;
foreach my $b (@fields) {
push @buttons, ucfirst( $b );
}
push @buttons, 'Quit';
my $answer = $mw->Dialog(
-title => 'XML-Resume-Library: Main Field Entry',
-text => 'Choose which field you wish to enter data for.',
-default_button => $default_button,
-buttons => [@buttons]
)->Show();
return if( $answer eq $default_button );
&GUI_get_field( $answer );
}
sub GUI_get_field {
my $field = shift;
$field = lc( $field );
&get_field( $Fields{$field}, $field );
}
sub get_field {
my $hash = shift;
my @fields = @_; # Like header/birth
my $fields_str;
my @subfields; # Like date
my $fields_str = '';
foreach my $f (@fields) {
$fields_str .= "{$f}";
}
print "fields($#fields)($#_) contains $fields_str\n";
# Our template (or DTD if you will) for this field, is in
# $Fields{$field}. This hash will consist of a Class key/value
# pair, and possibly other key/value pairs. The values may
# themselves be hashes in this latter case, possibly recursively.
# These subhashes will have Class keys to specify how often they
# can/should/may occur in the final resume.
my @key = keys( %{$hash} );
print "From $#key keys, we ";
my $n_ckeys = 0;
my $n_dkeys = 0;
foreach my $k (@key) {
$n_ckeys++ if( $k =~ /^[A-Z]/ ); # Count the "Control" keys
if( $k =~ /^[a-z]/ ) { # Count the "Data" field keys
$n_dkeys++ ;
push @subfields, $k;
}
}
print "found $n_ckeys control keys and $n_dkeys data keys\n";
return unless( my $Class = &get_CT( $hash, 'Class', $fields_str ) );
return unless( my $Type = &get_CT( $hash, 'Type', $fields_str ) );
print "$field_str: $Class;$type;$n_dkeys\n";
if( ($Type == 1) && ($n_dkeys == 0) ) {
print "Misconfigured Fields{$fields[-1]} - Is hash but has no data elements($Type)($n_ckeys)($n_dkeys)\n";
return;
}
if( $n_dkeys == 0 ) { # We are going to get either scalar or array stuff
if( exists( $hash->{Name} ) ) { # We need to name a generic field
# Just a scalar field
print "get_scalar_field on $fields_str:Name\n";
&get_scalar_field( $hash, $Class, $Type, 'Name', @fields );
}
# Query user for the data. Class determines how much (array context)
# data can be entered, and Type determines error checking. We are
# querying for @fields (for example header->name), and we want to
# use the textvariable $cache->{header}{name}.
if( ($Class == 3) || ($Class == -3) ) {
# We need to fetch or edit an array of data
print "get_array_field on $fields_str:Value\n";
&get_array_field( $hash, $Class, $Type, 'Value', @fields );
} else {
# Just a scalar field
print "get_scalar_field on $fields_str:Value\n";
&get_scalar_field( $hash, $Class, $Type, 'Value', @fields );
}
} else { # We are doing recursion
foreach my $k (@subfields) {
if( $Type == 1 ) {
# Just a container field, do recursion on sub-fields.
print "get_field (recursion) on $k\n";
&get_field( $hash->{$k}, @fields, $k );
} else {
print "Program error, subfield that isn't of type hash?\n";
}
}
}
}
sub get_CT {
my $hash = shift;
my $CT = shift;
my $fields_str = shift;
# my @fields = @_;
unless( exists( $hash->{$CT} ) ) {
print "Program error Fields", $fields_str, " has no $CT\n";
return undef;
}
return $hash->{$CT};
}
sub get_array_field {
my $hash = shift;
my $Class = shift; # 3 or -3
my $Type = shift;
my $NV = shift;
my @fields = @_;
my $label = join '->', @fields;
my $ans_array = [];
my $r_array;
my $c_str = '$cache->';
foreach my $f ($fields) {
$c_str .= "{$f}";
}
my $ref = eval "ref $c_str";
if( $ref ne 'ARRAY' ) {
print "Program error - reference type not array: $c_str = $ref\n";
return;
}
my $r_array = eval "$c_str";
my $stack = '';
foreach my $f (@{$r_array}) {
push @{$ans_array}, $f;
$stack .= "$f\n";
}
# We have a few areas in the box we display: Title-Bar, Label
# (name of field), Class Message, Type Message, Read-Only
# version of all "lines" entered so far, an area for entry of
# "current" line, and an area of buttons. The buttons are:
# Quit (keep original data), Save All data and quit, Zero this
# element of array, Zero All elements of array, Enter current
# data onto array and advance, Backup (towards beginning), and
# Advance for 7 buttons. Default is Enter current.
my $DefaultButton = 'Enter';
my @buttons = ( 'Save All', 'Zero Current', 'Zero All',
$DefaultButton, 'Backup 1', 'Advance 1',
'Quit',
);
my $EnterBox = $mw->DialogBox(
-title => 'XML_Resume-Library: Array Data Entry',
-buttons => [@buttons],
-default_button => $DefaultButton,
);
my $CText = $EnterBox->ROText(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
$CText->insert('end', $Message{Class}{$Class});
my $TText = $EnterBox->ROText(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
$TText->insert('end', $Message{Type}{$Type});
my $Label = $EnterBox->Label( -text => $label, -font => '9x15' )->pack();
my $SText = $EnterBox->ROText(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
$SText->insert('end', $stack);
# That's our Class text message, Type text message, Label, and the
# current "stack" of entries (for new, should be empty). What's
# left, the field for the user to edit, probably needs a format
# specific to the Type of data to enter. Just use Type for
# validation now.
my $UText = $EnterBox->Text(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
my $ptr = $#{$r_array}+1;
my $max = $ptr;
while( 1 ) {
my $action = $EnterBox->Show();
my $string = $UText->get('1.0', 'end');
if( $action eq 'Quit' ) {
return;
} elsif( $action eq 'Save All' ) {
# Validate current entry, push to stack, copy stack to $cache
# and return.
} elsif( $action eq 'Zero Current' ) {
# Set current to empty string. No undo!
} elsif( $action eq 'Zero All' ) {
# Set stack AND cache to empty. No undo!
} elsif( $action eq 'Backup 1' ) {
$ptr = $ptr > 0 ? $ptr-1 : 0;
} elsif( $action eq 'Advance 1' ) {
$ptr = $ptr < $max ? $ptr+1 : $max;
} elsif( $action eq 'Enter' ) {
# Validate current entry, push to stack
if( &validate( $string, $Type, @fields ) ) {
# This is element $ptr-1 of $max elements (last element
# is $max-1.
splice @{$ans_array}, $ptr, 1, $string;
# push @{$ans_array}, $string;
$max++ if( $ptr == $max );
$ptr++;
} else {
# Data doesn't validate, warn user.
}
} else {
# Never happens.
}
# Set our string to place in the user-editable frame.
$string = $ptr == $max ? '' : $ans_array->[$ptr];
# Adjust our stack display.
$stack = '';
foreach my $f (@{$ans_array}) {
$stack .= "$f\n";
}
# Delete current contents of stack frame, put in new contents
# and position "cursor".
$SText->delete('1.0', 'end');
$SText->insert('end', $stack);
$SText->see("$ptr.0");
}
}
sub get_scalar_field {
my $hash = shift;
my $Class = shift;
my $Type = shift;
my $NV = shift; # Name or Value
my @fields = @_;
my $c_str = '$cache->';
foreach my $f ($fields) {
$c_str .= "{$f}";
}
my $textvariable = eval "$c_str";
my $ref = ref $textvariable;
if( $ref ne '' ) { # or SCALAR ?
print "Program error - reference type not scalar: $c_str = $ref\n";
return;
}
my $title = $NV eq 'Value' ?
'XML-Resume-Library: Scalar Data Entry' :
'XML-Resume-Library: Name Generic Subfield';
my $t_str = join '->', @fields;
my $label = $NV eq 'Value' ?
$t_str : "Specific name for generic $t_str";
# We have a few areas in the box we display: Title-Bar, Label
# (name of field), Class Message, Type Message, Read-Only
# version of all "lines" entered so far, an area for entry of
# "current" line, and an area of buttons. The buttons are:
# Quit (keep original data), Save data and quit, Zero this
# element of array, Default is Save data and exit.
my $DefaultButton = 'Save';
my @buttons = ( $DefaultButton, 'Zero', 'Quit',
);
my $EnterBox = $mw->DialogBox(
-title => $title,
-buttons => [@buttons],
-default_button => $DefaultButton,
);
my $Label = $EnterBox->Label( -text => $label, -font => '9x15' )->pack();
my $TText = $EnterBox->ROText(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
$TText->insert('end', $Message{Type}{$Type});
my $CText = $EnterBox->ROText(
-width => 60,
-height => 3, #??
-relief => 'flat',
-wrap => 'word'
)->pack();
$CText->insert('end', $Message{Class}{$Class});
# That's our Class text message, Type text message, Label.
# What's left, the field for the user to edit, probably needs a format
# specific to the Type of data to enter. Just use Type for
# validation now.
my $UText = $EnterBox->Text(
-width => 60,
-height => 3, #??
-relief => 'sunken',
-wrap => 'word'
)->pack();
while( 1 ) {
my $action = $EnterBox->Show();
my $string = $UText->get('1.0', 'end');
if( $action eq 'Quit' ) {
return;
} elsif( $action eq 'Save' ) {
# Validate current entry and return.
if( &validate( $string, $Type, @fields ) ) {
# $cache... = $string;
return;
} else {
# Data doesn't validate, warn user.
}
} elsif( $action eq 'Zero' ) {
# Set current to empty string. No undo!
$textvariable = '';
} else {
# Never happens.
}
$UText->delete('1.0','end');
$UText->insert('end', $textvariable);
}
}
# All of of the GUI_get_@fields subroutines possibly referenced
# from above, need to produce a crude text rendering of the data
# in the field, for minimal display purposes.
sub GUI_get_header {
# Get a header. $**$
}
sub GUI_Display_Order {
# Build a DialogBox to put things into, with a purpose.
my @buttons = ('Set', 'Quit');
my $OrderBox = $mw->DialogBox(
-title => 'XML-Resume-Library: Field Display Order',
-buttons => [@buttons],
-default_button => 'Set',
);
my $purpose = 'Set the field display order. Leave blank to ignore a field.';
$OrderBox->Label( -textvariable => \$purpose )->pack();
# Find the longest field (in chars), to calculate field width of all labels.
# Is this width in pixels?
my $width = 0;
foreach my $s (@fields) {
my $l = length( $s );
$width = $l if( $l > $width );
}
# Build a Label and an Entry frame for each field.
foreach my $field (@fields) {
$OrderFrame{$field} = $OrderBox->Frame;
$OrderFrame{$field}->Label(
-text => $field,
-width => $width
)->pack(-side=> 'left' );
$OrderFrame{$field}->Entry(
-textvariable => \$cache->{field_order}{$field},
-width => 2
)->pack(-side=> 'left' );
$OrderFrame{$field}->pack();
}
# Display the Box to get users input.
$duplicates = 0;
while( 1 ) {
$action = $OrderBox->Show();
if( $action eq 'Set' ) {
my %index;
foreach $s (@fields) {
if( exists( $index{$cache->{field_order}{$s}} ) ) {
$duplicates = 1;
$cache->{field_order}{$s} = '';
} else {
$index{$cache->{field_order}{$s}} = $s;
}
}
# We've gone through the set of fields, were there any duplicates?
if( $duplicates ) {
my $Warn = $mw->Dialog(
-title => 'Error',
-text => 'You have an order index repeated.',
-default_button => 'Continue',
-buttons => ['Continue']
)->Show();
$duplicates = 0;
} else {
return;
}
} else { # Quit
foreach $s (@fields) {
$cache->{field_order}{$s} = '';
}
return;
}
} # End loop until we get valid input or user quits.
}
BEGIN {
# XML-Resume-Library 1.5.2
# Database is a collection of resumes. A resume consists of one or more of
# the following sections. Options, are optional parts of a resume. All
# parts can contain an attribute ID. A resume can contain zero or one of
# any option, and 0 or more of any section; according to the DTD. A schema
# could be much more restrictive/controlling. Skillareas is deprecated, so
# don't allow a person to insert it. Explode any found skillareas into the
# set of skillarea's.
# The Class key controls how many of this data type are allowed:
# 0 => deprecated, not replaced/edited
# 1 => 0 or 1 occurance of item
# 2 => 1 occurance must be found.
# 3 => 0 or more occurances
# -1 => deprecated 0 or 1 occurance, can be edited
# -2 => deprecated 1 occurance, can be edited (?)
# -3 => deprecated, 0 or more occurances, can be edited.
# The Type key controls the contents of the hash
# 0 => nothing, error.
# 1 => hash (sub elements present)
# 2 => string data, can be spell checked
# 3 => string data, no spell checking allowed (mostly names, etc.)
# 4 => enumerated, one of choice
# 5 => URL
# 6 => URI (same as URL ????)
# 7 => allowed suffix's in name (Jr, Sr., III, etc.)
# 8 => date
# 9 => Geographic location or address
# 10 => Phone/Fax/Pager (?) number, with Country/Area/etc. codes
# 11 => email
# 12 => instantMessage
# 13 => link
# 14 => inline_emphasis
# 15 => inline_citation
# 16 => period
# 17 => from_to (looks for word present, or date (7)
# Other miscellaneous stuff could be done, like components of degree.
# Leave for now as being too nitpicking to classify.
# 18 => degree
# XML-Resume-Library 1.5.2
%sections = (
'objective' => 1,
'history' => 1,
'academics' => 1,
'skillareas' => -1,
'skillarea' => 3, #? or 1?
'pubs' => 1,
'misc' => 1,
'referees' => 1,
'keywords' => 1,
'memberships' => 1,
'interests' => 1,
'clearances' => 1,
'awards' => 1,
);
@sections = keys( %sections );
%options = (
'docpath' => 1,
'header' => 1,
'lastModified' => 1,
'copyright' => 1,
);
@options = keys( %options );
%fields = (%sections, %options);
@fields = keys( %fields );
# 2nd Generation hash. Lower case first letter keys correspond to things in
# the DTD, upper case first letters to internal uses.
%Sections = (
'objective' => {
Class => 1,
},
'history' => 1,
'academics' => 1,
'skillareas' => -1,
'skillarea' => 3, #? or 1?
'pubs' => 1,
'misc' => 1,
'referees' => 1,
'keywords' => 1,
'memberships' => 1,
'interests' => 1,
'clearances' => 1,
'awards' => 1,
);
@Sections = keys( %Sections );
%Options = (
'docpath' => 1,
'header' => {
Class => 1,
Type => 1,
name => {
Class => 2,
Type => 1,
title => {
Class => 1,
Type => 3,
},
firstname => {
Class => 2,
Type => 3,
},
middlenames => {
Class => 1,
Type => 3,
},
surname => {
Class => 2,
Type => 3,
},
suffix => {
Class => 1,
Type => 4,
# Jr., Sr., III, etc.
},
},
address => {
Class => 1,
Type => 1,
country => {
Class => 2,
Type => 9,
},
postalCode => {
Class => 1,
Type => 9,
Name => 2, # Zip, Postal Code, etc. Probably determined programatically, not by user.
},
subcountry => {
Class => 1,
Type => 4,
Name => 2, # State, Province, etc. Programmatically.
},
subsubcountry => {
Class => 1,
Type => 4,
Name => 2, # County, Municipal District, Improvement District, etc. Programmatically.
},
municipality => {
Class => 1,
Type => 9,
Name => 2, # City, Town, etc. Programmatically.
},
submunicipality => {
Class => 1,
Type => 4,
Name => 2, # Suburb, ward, etc. Programmatically.
},
street => {
Class => 3,
Type => 9,
},
},
birth => {
Class => 1,
Type => 1,
date => {
Class => 2,
Type => 8,
},
},
contact => {
Class => 1,
Type => 1,
phone => {
Class => 3,
Type => 10,
},
fax => {
Class => 3,
Type => 10,
},
pager => {
Class => 3,
Type => 10, #???
},
email => {
Class => 3,
Type => 11,
},
url => {
Class => 3,
Type => 5,
},
instantMessage => {
Class => 3,
Type => 12,
},
},
},
'lastModified' => 1,
'copyright' => 1,
);
@Options = keys( %Options );
%Fields = (%Sections, %Options);
@Fields = keys( %Fields );
%Message = (
Class => {
0 => 'This class of data is deprecated and is not replaced. It will be ignored.',
1 => 'This item is optional, and if it is present, it can occur only once.',
2 => 'This item is mandatory, and must occur (only) once.',
3 => 'This is an array of items, you may enter as many instances of data of this type as you need to.',
'-1' => 'This optional data item is scheduled to be dropped in a later release. Please consider moving this kind of data elsewhere.',
'-2' => 'This mandatory item is scheduled to be dropped in a later release. Please consider moving this kind of data elsewhere.',
'-3' => 'This array of items is scheduled to be dropped in a later relase. Please consider moving this information elsewhere.',
},
Type => {
0 => 'Program error - this kind of data type should not occur.',
1 => 'Program error - this is a container for sub-data.',
2 => 'This item contains string type data which can be spell-checked.',
3 => 'This item contains string type data, but it should not be spell-checked.',
4 => 'This item contains enumerated data. You can choose what fits best.',
5 => 'This item contains a URL (Uniform Resource Locator).',
6 => 'This item contains a URI (Uniform Resource Identifier).',
7 => 'This item is meant for name suffixes, which should be enumerated.',
8 => 'This item contains a date (ISO YYYY/MM/DD).',
9 => 'This item contains a geographic location, including addresses.',
10 => 'This is a Telephone/Phone/Fax/Pager number.',
11 => 'This is an email address.',
12 => 'This is an Instant Message address (AIM/ICQ/...).',
13 => 'This item is a HTML or XML link.',
14 => 'This item is some kind of emphasized text for inline use.',
15 => 'This item is some kind of citation for inline use.',
16 => 'This item is a time period.',
17 => 'This item contains a range of dates, with the "To" date allowed to be "Present".',
18 => 'This is an education degree.',
},
);
}
__END__
I've been away from XML-Resume for a while. Mind you, I didn't contribute
much before anyway. :-)
I do think that XML Resume should move towards a schema, at least as
far as development goes. I would imagine there is an automated way
to generate a DTD from a schema, since there is almost an automated
way to get a schema from a DTD (w3c program called dtd2xsd.pl).
Since the schema contains more information about how often something
can occur, and the format it can take, it should be what is used
for development. I would also imagine, that sooner or later, a perl
module will surface will be able to generate a whole lot of input,
output and validation code based on an XML schema.
Having some data sitting in sections and some outside of sections (here
in my program called options), is kind of annoying from a program point
of view. I can see how it is more useful if someone is going to build
the XML file by hand however. In the case of automated spell-checking,
I do think have an attribute or element to cover the language used
for spelling would be useful. American is not the same as British
(and Canadian is mostly the union of the two), even though both
places speak "English".
Also, I still dislike all the various specialised components of an
address. I think a person should use generic names, and allow either
a program or the user, to specify the name of the "locality" or
component of the address.
For example, most of the world (including the US), understands what a
Postal Code is. And the US knows that a Zip code happens to be their
particular name for a Postal Code. Likewise, the largest division of
a country (politically) is generically known as a subcountry, and either
a program or a user can tell it if this component is to be called a
"state", a "province", or something else. So, we have:
Country obvious
Subcountry Province/State/etc.
Subsubcountry County/Municipal District/etc.
Municipality City/Town/Village
Submunicipality Suburb/Ward(?)
Street Building number, unit number, street number, etc.
So, we only need 6 elements, and to things like subcountry we attach
an attribute to "name" the element.