]> git.plutz.net Git - confetti/commitdiff
process split fields as arrays master
authorPaul Hänsch <paul@plutz.net>
Sat, 7 Sep 2024 11:10:52 +0000 (13:10 +0200)
committerPaul Hänsch <paul@plutz.net>
Sat, 7 Sep 2024 11:10:52 +0000 (13:10 +0200)
courses/export_pdf.sh
datetime.sh [new file with mode: 0755]
l10n.sh
ledgers/account.sh
lua/vcard.lua [new file with mode: 0644]
style.css

index 92e10c5f900626b0805da4224f3b5599bcbec039..10230b89c8457281a09d8c0fba8549cb75986cd4 100755 (executable)
@@ -90,9 +90,10 @@ style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"'
   ]
 ][body lang="de_DE"
   [table width="100%" style="page-break-after: always;"
   ]
 ][body lang="de_DE"
   [table width="100%" style="page-break-after: always;"
-    [col width=10*] [col width=5*] [col width=10*] [col width=15*]
+    [col width=2*] [col width=10*] [col width=5*] [col width=10*] [col width=15*]
     [thead
     [thead
-      [tr [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)] [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]]
+      [tr [th $style_td . $(l10n No.)] [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)]
+          [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]]
     ][tbody
       $(grep -F "${coursefile##*/}     " "$_DATA/mappings/attendance" |while read discard each; do
         vcf="$(pdi_load "$_DATA/vcard/$each")"
     ][tbody
       $(grep -F "${coursefile##*/}     " "$_DATA/mappings/attendance" |while read discard each; do
         vcf="$(pdi_load "$_DATA/vcard/$each")"
@@ -101,12 +102,16 @@ style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"'
                   [ "$type" ] && type="$(l10n "TYPE=$type"):"
                   printf '%s %s<br>' "$type" "$(pdi_value "$vcf" TEL $n)"
                 done  )"
                   [ "$type" ] && type="$(l10n "TYPE=$type"):"
                   printf '%s %s<br>' "$type" "$(pdi_value "$vcf" TEL $n)"
                 done  )"
-        printf '[tr valign=top [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \
+        printf '[tr valign=top [td %s .No @@No@@] [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \
+               "${style_td%\"} text-align: right;\"" \
                "$style_td" "$(pdi_value "$vcf" FN   |unescape |HTML)" \
                "$style_td" "$(pdi_value "$vcf" BDAY |unescape |HTML)" \
                "$style_td" "$tel" \
                "$style_td" "$(pdi_value "$vcf" NOTE |unescape |HTML)"
                "$style_td" "$(pdi_value "$vcf" FN   |unescape |HTML)" \
                "$style_td" "$(pdi_value "$vcf" BDAY |unescape |HTML)" \
                "$style_td" "$tel" \
                "$style_td" "$(pdi_value "$vcf" NOTE |unescape |HTML)"
-      done |sort)]
+      done |sort |while read -r line; do
+        attno=$((${attno-0} + 1))
+        printf '%s%2i%s\n' "${line%%@@No@@*}" $attno "${line#*@@No@@}"
+      done)]
   ]
   [table width="100%"
     [col width=30*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*]
   ]
   [table width="100%"
     [col width=30*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*]
diff --git a/datetime.sh b/datetime.sh
new file mode 100755 (executable)
index 0000000..2b4bba9
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/sh
+
+[ "$include_datetime" ] && return 0
+include_datetime="$0"
+
+# Copyright 2023 - 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+isdate(){
+  local date="$1" y m d
+
+  if   printf %s "$date" \
+    | grep -xEq '[0-9]{4}-((01|03|05|07|08|10|12)-(0[1-9]|[12][0-9]|3[01])|(04|06|09|11)-(0[1-9]|[12][0-9]|30)|02-(0[1-9]|[12][0-9]))'
+  then  # y-m-d (ISO Date)
+    y="${date%%-*}" d="${date##*-}" m="${date%-*}" m="${m#*-}"
+  elif printf %s "$date" \
+    | grep -xEq '((0?1|0?3|0?5|0?7|0?8|10|12)/(0?[1-9]|[12][0-9]|3[01])|(0?4|0?6|0?9|11)/(0?[1-9]|[12][0-9]|30)|0?2-(0[1-9]|[12][0-9]))/([0-9]{2}|[0-9]{4})'
+  then  # m/d/y (US Date)
+    y="${date##*/}" m="${date%%/*}" d="${date%/*}" d="${d#*/}"
+  elif printf %s "$date" \
+    | grep -xEq '((0?[1-9]|[12][0-9]|3[01])[\./](0?1|0?3|0?5|0?7|0?8|10|12)|(0?[1-9]|[12][0-9]|30)[\./](0?4|0?6|0?9|11)|(0[1-9]|[12][0-9])[\./]0?2)[\./]([0-9]{2}|[0-9]{4})'
+  then  # d/m/y or d.m.y (European Date / German Date)
+    y="${date##*.}" d="${date%%.*}" m="${date%.*}" m="${m#*.}"
+  else
+    return 1
+  fi
+  [ $y -lt 100 -a $y -gt 50 ] && y=$((y + 1900))
+  [ $y -lt 100 -a $y -le 50 ] && y=$((y + 2000))
+  date="$(printf "%04i-%02i-%02i" $y ${m#0} ${d#0})"
+
+  # leap year
+  if [ "$m" -eq 2 -a "$d" -eq 29 ]; then
+    if   [ "$((y % 400))" -eq 0 ]; then
+      :
+    elif [ "$((y % 100))" -eq 0 ]; then
+      return 1
+    elif [ "$((y % 4))" -eq 0 ]; then
+      :
+    else
+      return 1
+    fi
+  fi
+
+  printf '%04i-%02i-%02i\n' "$y" "${m#0}" "${d#0}"
+  return 0
+}
+
+istime(){
+  time="$1" h= m=
+
+  if   printf %s "$time" | grep -xEq '(0?[1-9]|1[012])(:[0-5][0-9])? ?(am|AM)\.?'; then
+    time="${time%?[aA][mM]}" h="${time%:*}" h="$(h % 12)"
+    [ "$h" != "$time" ] && m="${time#*:}" || m=0
+  elif printf %s "$time" | grep -xEq '(0?[1-9]|1[012])(:[0-5][0-9])? ?(pm|PM)\.?'; then
+    time="${time%?[aA][mM]}" h="${time%:*}" h="$(h % 12 + 12)"
+    [ "$h" != "$time" ] && m="${time#*:}" || m=0
+  elif printf %s "$time" | grep -xEq '(0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9]'; then
+    time="${time%?[aA][mM]}" h="${time%:*}" m="${time#*:}"
+  else
+    return 1
+  fi
+
+  printf '%02i:%02i\n' "${h#0}" "${m#0}"
+  return 0
+}
+
+numdays(){
+  # return number of days in a month (i.e. 28, 29, 30, or 31)
+  local y="$1" m="${2#0}"
+
+  case $m in
+    1|3|5|7|10|12)
+      printf 31\\n
+      ;;
+    4|6|8|9|11)
+      printf 30\\n
+      ;;
+    2) if  [ "$((y % 400))" -eq 0 ]; then
+        printf 29\\n
+      elif [ "$((y % 100))" -eq 0 ]; then
+        printf 28\\n
+      elif [ "$((y % 4))" -eq 0 ]; then
+        printf 29\\n
+      else
+        printf 28\\n
+      fi
+      ;;
+    *) return 1;;
+  esac
+}
diff --git a/l10n.sh b/l10n.sh
index 4f13ad48be4e563e50d46ab097f9280d7139ff75..e1c29ea75ac504e26ac37a51a02b37b92eddb8b2 100755 (executable)
--- a/l10n.sh
+++ b/l10n.sh
@@ -79,6 +79,7 @@ l10n_global() {
     sYEARLY) printf "Jährlich";;
 
     # UI labels
     sYEARLY) printf "Jährlich";;
 
     # UI labels
+    No.) printf %s "Nr.";;
     year) printf %s "Jahr";;
     month) printf %s "Monat";;
     day) printf %s "Tag";;
     year) printf %s "Jahr";;
     month) printf %s "Monat";;
     day) printf %s "Tag";;
@@ -132,11 +133,11 @@ l10n_global() {
     'Originator') printf %s "Auftraggeber";;
     'Reference Text') printf %s "Verwendungszweck";;
     'Amount') printf %s "Betrag";;
     'Originator') printf %s "Auftraggeber";;
     'Reference Text') printf %s "Verwendungszweck";;
     'Amount') printf %s "Betrag";;
-    'Balance') printf %s "Stand";;
+    'Balance') printf %s "Kto.Stand";;
     'Manual Record') printf %s "Manueller Eintrag";;
     'Manual Record') printf %s "Manueller Eintrag";;
-    'once') printf %s "einmalig";;
-    'monthly until') printf %s "monatlich bis";;
-    'until') printf %s "bis";;
+    'Recur Monthly') printf %s "[strike monat&shy;lich wie&shy;der&shy;holen]";;
+    'Credit Account') printf %s "Guthaben&shy;konto";;
+    'Submit') printf %s "Eintragen";;
 
     # UI Labels Special
     course_attendance) printf %s "Kurs&shy;teil&shy;nahme";;
 
     # UI Labels Special
     course_attendance) printf %s "Kurs&shy;teil&shy;nahme";;
index d9d3f4bd08771b606c2c587f5300d43ccace7ffc..9d3329936e08d25ee74c3248b7bff216bd913fb1 100755 (executable)
@@ -1,12 +1,11 @@
 #!/bin/sh
 
 credit() {
 #!/bin/sh
 
 credit() {
-  printf '%+03i\n' "$1" \
+  printf '%+04i\n' "$1" \
   | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;'
 }
 
   | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;'
 }
 
-if [ "$REQUEST_METHOD" = POST ]; then
-  uid="$(POST uid)"
+if uid="$(POST uid)"; then
   cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)"
   REDIRECT "${_BASE}/ledgers/account.sh?card=${cfile##*/}"
 fi
   cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)"
   REDIRECT "${_BASE}/ledgers/account.sh?card=${cfile##*/}"
 fi
@@ -15,20 +14,45 @@ fi
 . "${_EXEC}/pdiread.sh"
 . "${_EXEC}/cards/l10n.sh"
 . "${_EXEC}/cards/widgets.sh"
 . "${_EXEC}/pdiread.sh"
 . "${_EXEC}/cards/l10n.sh"
 . "${_EXEC}/cards/widgets.sh"
+. "${_EXEC}/datetime.sh"
 
 
-cardfile="${_DATA}/vcard/$(GET card |PATH)"
-if [ ! -f "$cardfile" ]; then
+cardfile="$(GET card |PATH)" cardfile="${cardfile##*/}"
+if [ ! -f "${_DATA}/vcard/$cardfile" ]; then
   SET_COOKIE 0 message="Invalid account: $cardfile"
   REDIRECT "${_BASE}/ledgers/"
 fi
 
   SET_COOKIE 0 message="Invalid account: $cardfile"
   REDIRECT "${_BASE}/ledgers/"
 fi
 
-cledger="${cardfile##*/}"
-cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account"
+cledger="${_DATA}/ledgers/vcf.${cardfile%.vcf}.account"
 
 
-{ card="$(pdi_load "$cardfile")"
+if tid="$(POST tid)"; then
+  if [ "$tid" != "$(transid "$cledger")" ]; then
+    SET_COOKIE 0 message="Ledger was changed since last edit"
+    REDIRECT "$REQUEST_URI"
+  fi
+  tdate="$(isdate "$(POST tdate)")"
+  tref="$(POST tref |grep -m1 -xE '.+')"
+  tamount="$(POST tamount \
+             | sed -E '
+             s;^([\+-]?[0-9]+)[\.,]([0-9][0-9])$;\1\2;;
+             s;^([\+-]?[0-9]+)$;&00;;
+           ' | grep -m1 -xE '[\+-]?[0-9]+')"
+  # debug "TDATE: $tdate TREF: $tref AMOUNT: $tamount"
+  if ! [ "$tdate" -a "$tref" -a "$tamount" ]; then
+    SET_COOKIE 0 message="Transaction info invalid"
+    REDIRECT "$REQUEST_URI"
+  fi
+  tdtstamp="$(date -ud "$tdate" +%s)"
+  printf '%s   %i      %s      \       %s      %i\n' \
+         "${tdate}" "${tdtstamp}" "$(STRING "${cardfile%.vcf}")" \
+         "$(STRING "$tref")" "${tamount}" \
+         >>"${cledger}"
+  REDIRECT "$REQUEST_URI"
+fi
+
+{ card="$(pdi_load "${_DATA}/vcard/$cardfile")"
   cat <<-EOF
        [h1 $(l10n Payments)]
   cat <<-EOF
        [h1 $(l10n Payments)]
-       [div .card #${cardfile##*/}
+       [div .card #${cardfile}
          [div .section .basic . $(
            card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO
          )]
          [div .section .basic . $(
            card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO
          )]
@@ -37,7 +61,7 @@ cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account"
          [div .section .address . $(card_item "$card" ADR X-IBAN)]
          [div .section .note    . $(card_item "$card" NOTE)]
          [div .section .attendance [h3 $(l10n course_attendance) ] [ul
          [div .section .address . $(card_item "$card" ADR X-IBAN)]
          [div .section .note    . $(card_item "$card" NOTE)]
          [div .section .attendance [h3 $(l10n course_attendance) ] [ul
-           $(grep -F "     ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do
+           $(grep -F " ${cardfile}" "$_DATA/mappings/attendance" |while read each discard; do
              printf '[li [a .item .attendance href="%s/courses#%s" . %s]]' \
                     "${_BASE}" "$each" \
                     "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)"
              printf '[li [a .item .attendance href="%s/courses#%s" . %s]]' \
                     "${_BASE}" "$each" \
                     "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)"
@@ -46,43 +70,57 @@ cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account"
          ]
        ]
        EOF
          ]
        ]
        EOF
-  printf '[table .transactions [thead
-          [tr [th .date . %s][th .orig . %s][th .reference . %s][th .amount . %s][th .balance . %s]]' \
-         "$(l10n Date)" "$(l10n Originator)" "$(l10n "Reference Text")" "$(l10n Amount)" "$(l10n Balance)"
-  printf '][tbody'
+  cat <<-EOF
+       [form method=POST
+         [hidden "tid" "$(transid "${cledger}")"]
+         [table .transactions
+           [thead [tr
+             [th .date . $(l10n Date)][th .orig . $(l10n Originator)]
+             [th .reference . $(l10n "Reference Text")]
+             [th .amount . $(l10n Amount)][th .balance . $(l10n Balance)]
+           ]]
+           [tbody
+       EOF
   cnt="$(pdi_count "$card" X-IBAN)"
   cnt="$(pdi_count "$card" X-IBAN)"
-  while [ "$cnt" -gt 0 ]; do
-    pdi_value "$card" X-IBAN "$cnt" |RXLITERAL
-    cnt=$((cnt - 1))
-  done \
-  | {
-    while read -r iban; do
-      grep -hE "^[^\t]+        [^\t]+  ${iban} " "${_DATA}/ledgers/"*.tbl
+  { while [ "$cnt" -gt 0 ]; do
+      pdi_value "$card" X-IBAN "$cnt" |RXLITERAL
+      cnt=$((cnt - 1))
     done
     done
-    if [ -f "$cledger" ]; then
-      :
-    fi
-  } \
+    RXLITERAL "${cardfile%.vcf}"; echo
+  } |debug \
+  | while read -r iban; do
+    grep -hE "^[^\t]+  [^\t]+  ${iban} " "${_DATA}/ledgers/"*
+  done \
   | sort -n -k2 \
   | { total=0
   | sort -n -k2 \
   | { total=0
-    while read -r date dtstamp iban accname subject amount; do
+    while read -r date dtstamp iban accname reftext amount; do
       total=$((total + amount))
       total=$((total + amount))
-      printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s][td .amount . %s][td .balance . %s]]' \
-             "$date" "$(HTML "$iban")" \
-             "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$subject" |HTML)" \
-             "$(credit "$amount")" "$(credit "$total")"
+      if [ "$iban" = "${cardfile%.vcf}" ]; then
+        printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s]
+                [td .amount . %s][td .balance . %s]]' \
+               "$date" "$(l10n "Credit Account")" \
+               "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$reftext" |HTML)" \
+               "$(credit "$amount")" "$(credit "$total")"
+      else
+        printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s]
+                [td .amount . %s][td .balance . %s]]' \
+               "$date" "$(HTML "$iban")" \
+               "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$reftext" |HTML)" \
+               "$(credit "$amount")" "$(credit "$total")"
+      fi
     done
   }
     done
   }
-  printf '[tr [th colspan=5 . %s]]' "$(l10n 'Manual Record')"
-  printf '[tr [td .date [input type=date placeholder="%s" name=tdate]]
-              [td .orig [input id=trec_once  type=radio name="trec" value="once" selected]
-                        [label for=trec_once  . %s]<br/>
-                        [input id=trec_month type=radio name="trec" value="month"]
-                        [label for=trec_month . %s]
-                        [input type=date name="trec_until" placeholder="%s"]]
-              [td .reference . [textarea placeholder="%s" name=treference]]
-              [td .amount colspan=2 [input type=number placeholder="%s" name=tamount value=0.00 step=.01]]' \
-       "$(l10n Date)" "$(l10n once)" "$(l10n "monthly until")" "$(l10n until)" "$(l10n "Reference Text")" "$(l10n Amount)"
-  printf ']]]'
+  cat <<-EOF
+       [tr [th colspan=5 . $(l10n 'Manual Record')]]
+       [tr [td .date
+              [input type=date placeholder="$(l10n Date)" name=tdate value="$(date +%F)"]
+             [input type=checkbox id=rr_month name=recur value=monthly]
+             [label for=rr_month $(l10n Recur Monthly)]
+           ][td .orig ]
+           [td .reference . [textarea placeholder="$(l10n "Reference Text")" name=tref]]
+           [td .amount [input type=number placeholder="$(l10n Amount)" name=tamount value=0.00 step=.01]]
+           [td .balance [button type=submit . $(l10n Submit)]]
+       ]]]]
+       EOF
 } \
 | yield_page ledgers
 } \
 | yield_page ledgers
diff --git a/lua/vcard.lua b/lua/vcard.lua
new file mode 100644 (file)
index 0000000..3066737
--- /dev/null
@@ -0,0 +1,171 @@
+#!/usr/bin/lua
+
+local Vcard = {
+  _component_fields = { N = true; GENDER = true; ADR = true; ORG = true; }
+}
+
+-- constructor, synonymous to load()
+function Vcard:new(path)
+  return self:load(path)
+end
+
+-- constructor
+-- load and parse vcard file, return new vcard object
+function Vcard:load(path)
+  local file, data
+
+  file = io.open(path)
+  if not file then return nil; end
+  data = file:read("a")
+  file:close()
+
+  return self:parse(data)
+end
+
+-- constructor
+-- parse text block of a vcard, return new vcard object
+function Vcard:parse(data)
+  local line, key, attr, value, i, v, n
+  local r = {}
+  setmetatable(r, { __index = self })
+
+  -- unwrap line continuations, remove carriage return at EOL
+  data = data:gsub("\r?\n[ \t]", "")
+  data = data:gsub("\r\n", "\n")
+
+  -- parse lines into fields
+  for line in data:gmatch("[^\n]+") do
+    key, attr, value = self:_parse_line(line)
+    if not r[key] then r[key] = { attr = {} } end
+    if Vcard._component_fields[key] then
+      table.insert(r[key], self:components(value));
+      r[key].attr[#r[key]] = attr
+      r[key][#r[key]][0] = value;
+    else for i,v in ipairs(self:_split_by(value, ",")) do
+      table.insert(r[key], v)
+      r[key].attr[#r[key]] = attr
+    end end
+  end
+
+  -- try to ensure existence of FN field
+  if not r["FN"] and r["N"] then
+    n = r["N"][1]
+    r["FN"] = { (n[4] or " ") .. " " .. (n[2] or " ") .. " " ..
+                (n[3] or " ") .. " " .. (n[1] or " ") .. " " ..
+                (n[5] or " ") }
+    r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
+  end
+  if not r["FN"] and r["NICKNAME"] then
+    r["FN"] = { r["NICKNAME"][1] }
+    r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
+  end
+
+  return r
+end
+
+-- split field components (i.e. the N field)
+function Vcard:components(line)
+  return self:_split_by(line, ";")
+end
+
+function Vcard:unescape(line)
+  local f local out = ""
+
+  while line ~= "" and line ~= "\\" do
+    if (function() f = line:match("^\\n([^\\]*)") return f end)() then
+      out = out .. "\n" .. f
+      line = line:sub(#f + 3)
+    elseif (function() f = line:match("^\\(.[^\\]*)") return f end)() then
+      out = out .. f
+      line = line:sub(#f + 2)
+    elseif (function() f = line:match("^([^\\]+)") return f end)() then
+      out = out .. f
+      line = line:sub(#f + 1)
+    end
+  end
+
+  return out
+end
+
+function Vcard:escape(line)
+  return line:gsub("\\", "\\\\"):gsub(",", "\\,"):gsub(";", "\\;"):gsub("\n", "\\n")
+end
+
+-- internal
+-- split vcard line into key, value, and an array of attributes
+function Vcard:_parse_line(line)
+  local key, value local attr = {}
+
+  key = line:match('^([^:;]+)[;:].*$')
+  if not key then return nil end
+
+  line = line:sub(#key + 1 )
+  a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$')
+  while a do
+    table.insert(attr, a)
+    line = line:sub(#a + 2)
+    a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$')
+  end
+
+  value = line:match("^:(.*)$")
+  if not value then return nil end
+
+  key = key:upper()
+  return key, attr, value
+end
+
+-- internal
+-- split string by specified character, unless the character is ecapped by \
+function Vcard:_split_by(line, split)
+  local f local r = { "" }
+
+  while line ~= "" and line ~= "\\" do
+    if line:match("^" .. split) then
+      table.insert(r, "")
+      line = line:sub(2)
+    end
+    f = line:match("^(\\.)") or line:match("^([^\\" .. split .. "]+)")
+    r[#r] = r[#r] .. f
+    line = line:sub(#f + 1)
+  end
+  return r
+end
+
+-- internal
+-- development tests
+function Vcard:_test()
+  local vcf = self:parse([=[
+BEGIN:VCARD
+VERSION:1.0
+UID:1
+N:Lastname;Firstname;Middle Names; Title; Suffix
+NICKNAME: Bone Crusher, Cookie Monster
+PHONE;TYPE=Home:123456789,666999
+PHONE;TYPE=Work;TYPE=Cell:987654321
+END:VCARD
+]=])
+
+  assert( vcf["PHONE"][1] == "123456789", "Phone/1 wrong number" )
+  assert( vcf["PHONE"][2] == "666999", "Phone/2 wrong number" )
+  assert( vcf["PHONE"].attr[2][1] == vcf["PHONE"].attr[1][1] )
+  assert( vcf["PHONE"][3] == "987654321", "Phone/3 wrong number" )
+  assert( vcf["PHONE"].attr[3][2] == "TYPE=Cell", "Phone/3 attr type=cell" )
+  assert( vcf["FN"][1] == "Title Firstname Middle Names Lastname Suffix" )
+  assert( vcf["N"][1][1] == "Lastname" )
+  assert( vcf["N"][1][4] == " Title" )
+
+  vcf = self:parse([=[
+BEGIN:VCARD
+VERSION:1.0
+UID:1
+NICKNAME: Bone Crusher, Cookie Monster
+PHONE;TYPE=Home:123456789,666999
+PHONE;TYPE=Work;TYPE=Cell:987654321
+END:VCARD
+]=])
+  assert( vcf["FN"][1] == "Bone Crusher" )
+
+  print("Vcard OK")
+end
+
+return Vcard
index c00829e2ba5b2fb8c4d45cc7575e261b15515b81..9e6bc02a04d5ef272978024eabb7d858bf97f5ea 100644 (file)
--- a/style.css
+++ b/style.css
@@ -504,7 +504,7 @@ body.ledgers .transactions td:nth-child(2n + 1) {
 }
 
 body.ledgers .transactions .date {
 }
 
 body.ledgers .transactions .date {
-  min-width: 8em;
+  width: 10em;
 }
 body.ledgers .transactions .orig span {
   display: block;
 }
 body.ledgers .transactions .orig span {
   display: block;
@@ -512,14 +512,18 @@ body.ledgers .transactions .orig span {
 body.ledgers .transactions .amount,
 body.ledgers .transactions .balance {
   vertical-align: bottom;
 body.ledgers .transactions .amount,
 body.ledgers .transactions .balance {
   vertical-align: bottom;
-  min-width: 6em;
+  width: 8em;
   text-align: right;
 }
 
 body.ledgers .transactions .reference textarea {
   width: 100%;
 }
   text-align: right;
 }
 
 body.ledgers .transactions .reference textarea {
   width: 100%;
 }
-body.ledgers .transactions .orig input[type=date],
+body.ledgers .transactions .date input + label {
+  display: inline-block;
+  vertical-align: middle;
+  width: 7em;
+}
 body.ledgers .transactions .date input[type=date],
 body.ledgers .transactions .amount input[type=number] {
   display: block;
 body.ledgers .transactions .date input[type=date],
 body.ledgers .transactions .amount input[type=number] {
   display: block;