#!/usr/bin/perl # # ColdSync conduit. Converts Palm Memos to a plain text format and # back again. # # Copyright (C) 2000, Andrew Arensburger. # You may distribute this file under the terms of the Artistic # License, as specified in the README file. # # $Id: memo-text,v 1.4 2001/02/18 06:50:39 arensb Exp $ # XXX - Write pod. # XXX - If memo contains NUL, truncate it. # XXX - Would be nice to replace \x95 with some other character (and # back). # XXX - This conduit can't create the database from scratch. It # should. # XXX - Headers # "Where: " - Specifies location of memos # "Scheme: [all|categories|files]" # all - All memos in one file. "Where" must be that file # categories - "Where" is a directory. Each file in that # directory contains all memos in one category. # files - "Where" is a directory. It contains a set of # directories named after categories. Each file in # "Where"/ is a single memo # "Skip: N" - Skip N lines at the beginning of the file. Those lines # should be preserved. This is for XPostIt notes. # "Delete: [yes|no|archive|expunge]" - Default is "no": don't delete # anything, just add/update existing memos. "yes" and # "archive" are synonyms. # XXX - "__END__" terminates the file. That way, you can put Emacs # magic lines at the end. use strict; use Palm::Memo; use ColdSync; # Set default values %HEADERS = ( File => "$ENV{HOME}/Memos", Delete => "no", # [yes|no|archive|expunge] # XXX - Same as todo-text # XXX - Should be able to specify multiple schemes: # - One file containing all memos # - One file per category # - One file per memo ); my $VERSION = (qw( $Revision: 1.4 $ ))[1]; # Cute hack for conduit version ConduitMain( "fetch" => \&DoFetch, "dump" => \&DoDump, ); sub DoFetch { local $/; # XXX - Create the output database if necessary. This can # happen if OutputDB wasn't specified, or if this is a first # sync and the database doesn't exist. open IN, "< $HEADERS{File}" or die "401 Can't open \"$HEADERS{File}\": $!\n"; $/ = "\nMemo"; # Read the headers. my $header = ; my @catlines; my @categories; my @uniqueIDs; my %cat2num; # Maps category names to category number chomp $header; # Remove trailing "\nMemo "; # Find the category lines in the header @catlines = grep { /^\s*Categories:/i .. /^\s*END/i } split /\n/, $header; if ($catlines[0] !~ /^\s*Categories:/i) { # XXX - This can't happen. @catlines might be empty, # but $catlines[0] has to be "\s*Categories:". warn "Missing Categories: line in header\n"; } else { shift @catlines; # Discard ".Categories" } if ($catlines[$#catlines] !~ /^\s*END/i) { warn "Missing END line in header\n"; } else { pop @catlines; # Discard ".END" } # XXX - Keep only the first 16 categories. Warn if there are more. # Initialize the categories my $i = 0; for (@catlines) { my $name; # Category name my $id; # Category ID if (!/^\s*(\d+):\s*(.*)/) { warn "Malformed category line: \"$_\", line $.\n"; next; } $id = $1; $name = $2; $name =~ s/\s*$//; # Trim trailing whitespace $cat2num{$name} = $i; # Remember category number, for later push @categories, $name; push @uniqueIDs, $id; $i++; } @{$PDB->{appinfo}{categories}} = @categories; @{$PDB->{appinfo}{uniqueIDs}} = @uniqueIDs; # Now read the memos themselves my $memo; while ($memo = ) { # The following substitution may seem odd (why not # just use "chomp $memo"?) We also want to remove the # trailing \n on the last memo, otherwise it'll grow # by a \n each time we sync. $memo =~ s/\Memo$//; $memo =~ s/\n$//; my $record; my $id = 0; # Memo's ID my $category = "Unfiled"; # Memo's category name my $private = 0; # Is the memo private? my $top; # First line of the memo if ($memo !~ /\n/) { warn "Malformed memo $.\n"; next; } $top = $`; $memo = $'; $top =~ s/^\s+//; # Trim leading whitespace $top =~ s/\s+$//; # Trim trailing whitespace $memo =~ s/\\(.)/$1/g; # Remove extraneous backslashes # XXX - Would be nice to allow \039 or \xf3 style # escapes. However, make sure it plays nice with the # other substitutions. my $topfield; # Item in the top line # This ugly line splits $top at the commas. The # parenthesized expression in there is to prevent # splits at escaped commas: "\,". foreach $topfield (split /\s*(?findRecordByID($id)) == undef) { # Either the ID wasn't specified, it was given # as 0, or it doesn't exist in the database. # Either way, we need to create a new record. $record = $PDB->append_Record; $record->{id} = 0; # XXX - Assign a new ID? $record->{attributes}{dirty} = 1; } # Mark this memo as having been seen. We do it right # away so that it won't be deleted if, for some # reason, the rest of this loop aborts, but the # deletion phase happens anyway. $record->{_seen} = 1; # See if the record's category has changed if ($cat2num{$category} != $record->{category}) { $record->{category} = $cat2num{$category}; $record->{attributes}{dirty} = 1; } # See if the "private" flag has changed. # The "xor" here effectively converts the two sides of # the expressions into booleans before comparing them. # The "xor" basically acts as "ne" or "!=", but is # more robust. if ($private xor $record->{attributes}{private}) { # The "private" flag has changed $record->{attributes}{private} = $private; $record->{attributes}{dirty} = 1; } # The important part: see if the text of the memo has # changed. if ($memo ne $record->{data}) { $record->{data} = $memo; $record->{attributes}{dirty} = 1; } } close IN; # XXX - Delete any records that haven't been seen (don't have # a "_seen" member. But only if the headers say to do so. } sub DoDump { my $i; # XXX - Read the original output file. Copy the first # $HEADERS{Skip} lines. open OUT, "> $HEADERS{File}" or die "401 Can't open \"$HEADERS{File}\": $!\n"; # Dump category names. print OUT "Categories:\n"; for ($i = 0; $i <= $#{$PDB->{appinfo}{categories}}; $i++) { my $category = $PDB->{appinfo}{categories}[$i]{name}; my $cat_id = $PDB->{appinfo}{categories}[$i]{id}; next if $category eq ""; $category =~ s/,/\\,/g; # Escape commas print OUT " $cat_id: $category\n"; } print OUT "END\n"; my $record; foreach $record (@{$PDB->{records}}) { my @f; print OUT "Memo "; print OUT "ID: ", $record->{id}; print OUT ", Category: ", $PDB->{appinfo}{categories}[$record->{category}]{name}; print OUT ", Private" if $record->{attributes}{private}; print OUT "\n"; my $text = $record->{data}; # Escape any inconvenient characters in the memo text $text =~ s/\\/\\\\/g; $text =~ s/^Memo/\\Memo/gm; # Escape leading "Memo" print OUT $text, "\n"; } close OUT; }