diff --git a/perl/Gnome-Tomboy/lib/Tomboy/Note/Simple.pm b/perl/Gnome-Tomboy/lib/Tomboy/Note/Simple.pm index bfd39629e183a7081dfcb9813f567dc3f507c151..72c118f663a4cecd937f4b7f1b4c0fc24a7fdbe1 100755 --- a/perl/Gnome-Tomboy/lib/Tomboy/Note/Simple.pm +++ b/perl/Gnome-Tomboy/lib/Tomboy/Note/Simple.pm @@ -25,7 +25,7 @@ Simple abstraction for notes written with Gnome's Tomboy. The script uses XML::Parser in Tree style and uses it's parse tree as the note's content datastructure (stored in "text"). -=head1 BUGS +=head1 NOTES This module consists of originally two different ones, they are not completely consistent. The difference is how the content is stored. @@ -58,6 +58,8 @@ $Data::Dumper::Indent= 1; use Tomboy; use Util::XML_Parser_Tree; +my $VERSION= 0.004; + my %fields= ( 'title' => {}, @@ -82,6 +84,25 @@ my @fields_seq2= (@fields_default2); my ($s_text, $e_text)= ('<text xml:space="preserve">', '</text>'); my ($s_note_content, $e_note_content)= ('<note-content version="0.1">', '</note-content>'); +my $empty_text= +[ + { + 'xml:space' => 'preserve' + }, + 'note-content', + [ + { + 'version' => '0.1' + } + ] +]; + +=head2 new ((attributes => values)*) + +Create a new (empty) note and optionlly set attributes + +=cut + sub new { my $class= shift; @@ -89,8 +110,13 @@ sub new my $title= 'New Note ' . Tomboy::ts_ISO (); my $note= { - 'lines' => [], 'title' => $title, + 'lines' => [], + 'text' => undef, + + # flags to indicate if 'lines' or 'text' is up to date: + 'flg_text' => 1, + 'flg_lines' => 1, }; foreach my $f (@fields_date) { $note->{$f}= Tomboy::ts_ISO() } foreach my $f (@fields_default1) { $note->{$f}= $fields{'default'} } @@ -101,6 +127,12 @@ sub new $note; } +=head2 $note->set ((attributes => values)*) + +Set attribute values without checking. + +=cut + sub set { my $note= shift; @@ -114,30 +146,35 @@ sub set =cut +# dunno if this is really useful sub empty_text { my $note= shift; my $title= shift || 'empty text'; - $note->{'text'}= [ - { - 'xml:space' => 'preserve' - }, - 'note-content', - [ - { - 'version' => '0.1' - }, - 0, - $title, - ] - ]; +=begin comment + + my $x= $note->{'text'}= $empty_text; + push (@{$x->[1]}, '0', $title); + +=end comment +=cut + $note->{'title'}= $title; $note->{'lines'}= [ $title ]; + $note->parse_lines(); 1; } +=head2 $note->parse ($filename) + +=head2 $note= parse Tomboy::Note::Simple ($filename) + +Parse given file using XML::Parser in "Tree" style. + +=cut + sub parse { my $c= shift; @@ -159,7 +196,7 @@ sub parse $note->{'fnm'}= $fnm; - my $p= new XML::Parser (Style => 'Tree'); + my $p= new XML::Parser (Style => 'Tree'); # ProtocolEncoding should be derived from the file's PI # print "p: ", Dumper ($p); my $l1; eval { $l1= $p->parsefile($fnm, ErrorContext => 3) }; @@ -234,28 +271,48 @@ sub parse } } + $note->{'flg_text'}= 1; + $note->{'flg_lines'}= 0; + $note; } =head1 Group 1+2: glue +=head2 $note->update() + +Refresh 'text' or 'lines' if one of them is outdated. + =cut -sub text_to_lines +sub update { my $note= shift; - my $x= $note->{'text'}; + if ($note->{'flg_text'}) { $note->text_to_lines(); } + elsif ($note->{'flg_lines'}) { $note->parse_lines(); } + else { return undef; } - # print "x: ", Dumper($x); + 1; +} - my $nc= $x->[2]; - # print "nc: ", Dumper($nc); - shift (@$nc); # remove the text-element's attributes +=head2 $note->text_to_lines() + +Refresh 'lines' from 'text'. + +Compare with set_lines() below. + +=cut + +sub text_to_lines +{ + my $note= shift; + + my $nc= $note->get_note_content(); my $s= Util::XML_Parser_Tree::to_string (@$nc); - # split drops the new lines at the end, so we need to go the extra mile - my $cnt= length ($1) if ($s=~ s#(\n+)$##); + # split drops the newlines at the end, so we need to go the extra mile + my $cnt= ($s=~ s#(\n+)$##) ? length ($1) : 0; my @s= split ("\n", $s); for (my $i= 1; $i < $cnt; $i++) { push (@s, '') } @@ -267,42 +324,171 @@ sub text_to_lines # TODO: compare existing title $note->{'title'}= $title unless ($note->{'title'}); $note->{'lines'}= \@s; + # NOTE: maybe setting a proper title should be a separate method # ($title, @s); + $note->{'flg_text'}= 1; # if 'lines' are generated from 'text', then both must be up-to-date + $note->{'flg_lines'}= 1; + 1; } +=head2 $note->parse_lines() + +Refresh 'text' from 'lines'. + +=cut + sub parse_lines { my $note= shift; # print "text: ", Dumper ($note->{'text'}); my @lines= @{$note->{'lines'}}; - my $start= join ('', $s_text, $s_note_content, shift (@lines)); - my $x= parse_string (join ("\n", $start, @lines, join ('', $e_note_content, $e_text))); + + my $x= parse_string (wrap_lines ($note->{'lines'})); + $note->{'text'}= $x->[1]; + + $note->{'flg_text'}= 1; # if 'text' is parsed from 'lines', then both must be up-to-date + $note->{'flg_lines'}= 1; + + 1; } -sub parse_string +=head1 ACCESSORS + +=head2 $old_title= $note->set_title ($new_title) + +Update the title of a note. + +Currently, the title is not sanitized at all. + +=cut + +sub set_title { - my $str= shift; + my $note= shift; + my $title= shift; - # print "str=[$str]\n"; - my $p= new XML::Parser (Style => 'Tree'); - # print "p: ", Dumper ($p); - my $l1; - eval { $l1= $p->parsestring($str, ErrorContext => 3) }; - if ($@) - { - print "parsestring failed str=[$str]:\n", $@, "\n"; - return undef; + my $old_title= $note->{'title'}; + + # TODO: sanitize the title (e.g. remove XML tags which are sometimes + # present in the note's first line) + $note->{'title'}= $title; + + $old_title; +} + +=head2 $xml_tree= $note->get_text() + +Retrieves the 'text' component (refreshing it, if necessary) and returns +the XML::Parser tree structure. + +=cut + +sub get_text +{ + my $note= shift; + + $note->update() unless ($note->{'flg_text'}); + # my $t= $note->get_note_content(); + my @t= @{$note->{'text'}}; + shift (@t); + (wantarray) ? @t : \@t; +} + +sub set_text +{ + my $note= shift; + my $new_text= shift; + + my $old_text= $note->{'text'}; + my @new_text= ($old_text->[0], @$new_text); + $note->{'text'}= \@new_text; + + $note->{'flg_lines'}= 0; + $note->{'flg_text'}= 1; + + $old_text; +} + +=head2 $line_list= $note->get_lines() + +Retrieves the 'lines' component (refreshing it, if necessary) and returns +the a hash ref of all lines + +=cut + +sub get_lines +{ + my $note= shift; + + $note->update() unless ($note->{'flg_lines'}); + $note->{'lines'}; +} + +sub set_lines +{ + my $note= shift; + my $new_lines= shift; + + unless (ref ($new_lines) eq 'ARRAY') + { # we want an array ref, fix that + + # split drops the newlines at the end, so we need to go the extra mile + my $cnt= ($new_lines=~ s#(\n+)$##) ? length ($1) : 0; + my @s= split ("\n", $new_lines); + for (my $i= 1; $i < $cnt; $i++) { push (@s, '') } + + $new_lines= \@s; } - # print "l1: ", Dumper ($l1); - $l1; + + my $old_lines= $note->{'lines'}; + $note->{'lines'}= $new_lines; + + $note->{'flg_lines'}= 1; + $note->{'flg_text'}= 0; + + $old_lines; +} + +sub get_note_content +{ + my $note= shift; + + my $x= $note->{'text'}; + # print "x: ", Dumper($x); + + my @nc= @{$x->[2]}; + # print "nc: ", Dumper(\@nc); + shift (@nc); # remove the text-element's attributes + + (wantarray) ? @nc : \@nc; +} + +sub set_note_content +{ + my $note= shift; + my $new_nc= shift; + + my $x= $note->{'text'}; + my $old_nc= $x->[2]; + my @new_nc= ($old_nc->[0], @$new_nc); + $x->[2]= \@new_nc; + + # TODO/NOTE: update flags? + + $old_nc; } + =head1 Group 2: text generator +=head2 $note->add_lines ( array of text lines ) + +Push additional lines to 'lines', invalidates 'text'. + =cut sub add_lines @@ -317,21 +503,38 @@ sub add_lines push (@{$note->{'lines'}}, @lines); } + $note->{'flg_lines'}= 1; + $note->{'flg_text'}= 0; + $note->{'e_updated'}= time(); } +=head2 $note->save ($out_dir|undef, $out_fnm|undef) + +Save the note, the filename can either be specified or will be generated. +Both $out_dir and $out_fnm are optional but $out_fnm takes precedence. + +=cut + sub save { my $note= shift; my $out_dir= shift; my $fnm_out= shift; - my ($title, $uuid, $ts_updated, $ts_md_updated, $ts_created, $e_updated, $lines, $is_template, $nb_name)= - map { $note->{$_} } qw(title uuid last-change-date last-metadata-change-date create-date e_updated lines is_template notebook); + # refresh lines, if they are not up-to-date + $note->update() unless ($note->{'flg_lines'}); + + my ($title, $uuid, $lines, $ts_updated, $ts_md_updated, $ts_created, + $e_updated, $is_template, $nb_name)= + map { $note->{$_} } qw(title uuid lines last-change-date + last-metadata-change-date create-date e_updated is_template + notebook); # sanitize data $note->{'uuid'}= $uuid= Tomboy::get_uuid() unless ($uuid); $note->{'title'}= $title= $uuid unless ($title); + # NOTE: Hmm... maybe we should use the first line here. if ($e_updated) { @@ -348,7 +551,7 @@ sub save unless (defined ($fnm_out)) { - $fnm_out= $out_dir if ($out_dir); + $fnm_out= $out_dir.'/' if ($out_dir); $fnm_out.= $uuid . '.note'; } @@ -365,7 +568,7 @@ sub save <?xml version="1.0" encoding="utf-8"?> <note version="0.3" xmlns:link="http://beatniksoftware.com/tomboy/link" xmlns:size="http://beatniksoftware.com/tomboy/size" xmlns="http://beatniksoftware.com/tomboy"> EOX - print FO ' <title>'. Util::XML_Parser_Tree::tlt($title) ."</title>\n"; + print FO ' <title>'. Util::XML_Parser_Tree::tlt_str($title) ."</title>\n"; print FO ' ', $s_text, $s_note_content; foreach my $line (@$lines) @@ -398,6 +601,15 @@ EOX $fnm_out; } +=head1 INTERNAL FUNCTIONS + +=head2 print_attribute ($fh, $note, $field) + +Print a XML rendering of given Tomboy attribute to $fh, apply (and set) +default values, if value is not defined. + +=cut + sub print_attribute { local *F= shift; @@ -409,6 +621,7 @@ sub print_attribute { my $x= $fields{$f}; return if (exists ($x->{'supress'})); # supress the default for that one + my $b; if (exists ($x->{'default'})) { $b= $x->{'default'} } # TODO: elsif exists function .... @@ -418,27 +631,63 @@ sub print_attribute print F ' <', $f, '>', $a, '</', $f, ">\n"; } -__END__ +sub wrap_lines +{ + my $l= shift; -=head1 AUTHOR + my @lines; + if (ref ($l) eq 'ARRAY') { @lines= @$l; } + else { @lines= ($l, @_); } # assume we received an array - Gerhard Gonter <ggonter@gmail.com> + my $start= join ('', $s_text, $s_note_content, shift (@lines)); + join ("\n", $start, @lines, join ('', $e_note_content, $e_text)); +} -=head1 BUGS +=head2 parse_string ($str) -* XML::Parser throws exceptions, these are currently not handled well. +Uses XML::Parser in "Tree" style to parse a text block (multiple lines) +in one string. + +Returns the parse tree or undef. =cut - <tags> - <tag>system:notebook:Kalender 2014</tag> - </tags> +sub parse_string +{ + my $str= shift; + + # print "str=[$str]\n"; + my $p= new XML::Parser (Style => 'Tree', 'NoExpand' => 1); + # print "p: ", Dumper ($p); + my $l1; + eval { $l1= $p->parse($str, ErrorContext => 3, 'ProtocolEncoding' => 'UTF-8') }; + if ($@) + { + print "parsestring failed str=[$str]:\n", $@, "\n"; + return undef; + } + # print "l1: ", Dumper ($l1); -Template: - <tags> - <tag>system:template</tag> - <tag>system:notebook:Kalender 2014</tag> - </tags> + $l1; +} 1; +__END__ + +=head1 AUTHOR + + Gerhard Gonter <ggonter@cpan.org> + +=head1 BUGS, PROBLEMS, NOTES + + * XML::Parser throws exceptions, these are currently not handled well. + * The last newline in the note tends to be removed, however, the note + will end with one newline, if there was none before. + * $note->save() will not use the same filename from the parse() method, + instead, a new one will be generated. You have to specify the + filename, if need, e.g. $note->save(undef, $note->{'fnm'}); + * The POD needs some attention. + +=cut +