From 9f4db5a4475607d0d710500f236202e0b3c22628 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Thu, 23 May 2024 13:12:18 +0200 Subject: [PATCH 1/6] styles and translations ledgers --- l10n.sh | 8 ++++---- style.css | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/l10n.sh b/l10n.sh index 4f13ad4..039cb6f 100755 --- a/l10n.sh +++ b/l10n.sh @@ -132,11 +132,11 @@ l10n_global() { '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";; - 'once') printf %s "einmalig";; - 'monthly until') printf %s "monatlich bis";; - 'until') printf %s "bis";; + 'Recur Monthly') printf %s "[strike monat­lich wie­der­holen]";; + 'Credit Account') printf %s "Guthaben­konto";; + 'Submit') printf %s "Eintragen";; # UI Labels Special course_attendance) printf %s "Kurs­teil­nahme";; diff --git a/style.css b/style.css index c00829e..9e6bc02 100644 --- a/style.css +++ b/style.css @@ -504,7 +504,7 @@ body.ledgers .transactions td:nth-child(2n + 1) { } body.ledgers .transactions .date { - min-width: 8em; + width: 10em; } 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; - min-width: 6em; + width: 8em; 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; -- 2.39.2 From 6660ac34581deea50b81efa696e04015cc2298bb Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Thu, 22 Aug 2024 15:07:52 +0200 Subject: [PATCH 2/6] vcard parser in lua --- lua/vcard.lua | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 lua/vcard.lua diff --git a/lua/vcard.lua b/lua/vcard.lua new file mode 100644 index 0000000..1794c06 --- /dev/null +++ b/lua/vcard.lua @@ -0,0 +1,109 @@ +#!/usr/bin/lua + +local Vcard = {} + +-- 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 + local r = {} + + -- 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 + for i,v in ipairs(self:_split_line(value)) do + table.insert(r[key], v) + r[key].attr[#r[key]] = attr + end + end + + setmetatable(r, { __index = self }) + return r +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 value fields aggregated by comma ignoring escaped commas +function Vcard:_split_line(line) + local f + local r = { "" } + + while line ~= "" and line ~= "\\" do + if line:match("^,") then + table.insert(r, "") + line = line:sub(2) + end + f = line:match("^(\\.)") or line:match("^([^\\,]+)") + r[#r] = r[#r] .. f + line = line:sub(#f + 1) + end + return r +end + +-- internal +-- development tests +function Vcard:_test() + local vcf = Vcard:parse([=[ +BEGIN:VCARD +VERSION:1.0 +UID:1 +N:Lastname;Firstname;Middle Names; Title; Suffix +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" ) +end + +return Vcard -- 2.39.2 From d211d006299acb28827bf99eaa209cc826e0d0c1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Thu, 5 Sep 2024 13:40:51 +0200 Subject: [PATCH 3/6] number field in pdf attendee list --- courses/export_pdf.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/courses/export_pdf.sh b/courses/export_pdf.sh index 92e10c5..10230b8 100755 --- a/courses/export_pdf.sh +++ b/courses/export_pdf.sh @@ -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;" - [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 - [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")" @@ -101,12 +102,16 @@ style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"' [ "$type" ] && type="$(l10n "TYPE=$type"):" printf '%s %s
' "$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)" - 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*] -- 2.39.2 From d1865898a2c380561b259aaf9600d131b6936c2f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Thu, 5 Sep 2024 13:45:20 +0200 Subject: [PATCH 4/6] number field in pdf attendee list --- l10n.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/l10n.sh b/l10n.sh index 039cb6f..e1c29ea 100755 --- a/l10n.sh +++ b/l10n.sh @@ -79,6 +79,7 @@ l10n_global() { sYEARLY) printf "Jährlich";; # UI labels + No.) printf %s "Nr.";; year) printf %s "Jahr";; month) printf %s "Monat";; day) printf %s "Tag";; -- 2.39.2 From 870866afe1c0271d71fdbafe8f5a93e2e0929d99 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Thu, 5 Sep 2024 14:23:52 +0200 Subject: [PATCH 5/6] lua field splitting and escaping --- lua/vcard.lua | 76 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/lua/vcard.lua b/lua/vcard.lua index 1794c06..ab81248 100644 --- a/lua/vcard.lua +++ b/lua/vcard.lua @@ -23,7 +23,7 @@ end -- constructor -- parse text block of a vcard, return new vcard object function Vcard:parse(data) - local line, key, attr, value, i, v + local line, key, attr, value, i, v, n local r = {} -- unwrap line continuations, remove carriage return at EOL @@ -34,21 +34,61 @@ function Vcard:parse(data) for line in data:gmatch("[^\n]+") do key, attr, value = self:_parse_line(line) if not r[key] then r[key] = { attr = {} } end - for i,v in ipairs(self:_split_line(value)) do + for i,v in ipairs(self:_split_by(value, ",")) do table.insert(r[key], v) r[key].attr[#r[key]] = attr end end + -- try to ensure existence of FN field + if not r["FN"] and r["N"] then + n = self:components(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 + setmetatable(r, { __index = self }) 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 = {} + local key, value local attr = {} key = line:match('^([^:;]+)[;:].*$') if not key then return nil end @@ -69,17 +109,16 @@ function Vcard:_parse_line(line) end -- internal --- split value fields aggregated by comma ignoring escaped commas -function Vcard:_split_line(line) - local f - local r = { "" } +-- 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("^,") then + if line:match("^" .. split) then table.insert(r, "") line = line:sub(2) end - f = line:match("^(\\.)") or line:match("^([^\\,]+)") + f = line:match("^(\\.)") or line:match("^([^\\" .. split .. "]+)") r[#r] = r[#r] .. f line = line:sub(#f + 1) end @@ -89,11 +128,12 @@ end -- internal -- development tests function Vcard:_test() - local vcf = Vcard:parse([=[ + 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 @@ -104,6 +144,20 @@ END:VCARD 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" ) + + 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 -- 2.39.2 From 237ef0b0cea6cf716876c008ce689cdf22e9d023 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Sat, 7 Sep 2024 13:10:52 +0200 Subject: [PATCH 6/6] process split fields as arrays --- lua/vcard.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lua/vcard.lua b/lua/vcard.lua index ab81248..3066737 100644 --- a/lua/vcard.lua +++ b/lua/vcard.lua @@ -1,6 +1,8 @@ #!/usr/bin/lua -local Vcard = {} +local Vcard = { + _component_fields = { N = true; GENDER = true; ADR = true; ORG = true; } +} -- constructor, synonymous to load() function Vcard:new(path) @@ -25,6 +27,7 @@ end 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]", "") @@ -34,15 +37,19 @@ function Vcard:parse(data) for line in data:gmatch("[^\n]+") do key, attr, value = self:_parse_line(line) if not r[key] then r[key] = { attr = {} } end - for i,v in ipairs(self:_split_by(value, ",")) do + 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 end -- try to ensure existence of FN field if not r["FN"] and r["N"] then - n = self:components(r["N"][1]) + n = r["N"][1] r["FN"] = { (n[4] or " ") .. " " .. (n[2] or " ") .. " " .. (n[3] or " ") .. " " .. (n[1] or " ") .. " " .. (n[5] or " ") } @@ -53,7 +60,6 @@ function Vcard:parse(data) r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "") end - setmetatable(r, { __index = self }) return r end @@ -145,6 +151,8 @@ END:VCARD 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 -- 2.39.2