Skip to content

Commit

Permalink
Support <Ask> and <Select> prompts in templates
Browse files Browse the repository at this point in the history
Ref #375
  • Loading branch information
martinhpedersen committed Jan 20, 2024
1 parent 04ef16e commit c409d48
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 38 deletions.
130 changes: 92 additions & 38 deletions internal/forms/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"log"
"math"
"net/http"
"net/textproto"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -505,7 +506,6 @@ func (m *Manager) ComposeForm(tmplPath string, subject string) (MessageForm, err
formValues := map[string]string{
"subjectline": subject,
"templateversion": m.getFormsVersion(),
"msgsender": m.config.MyCall,
}
fmt.Printf("Form '%s', version: %s", form.TxtFileURI, formValues["templateversion"])
formMsg, err := formMessageBuilder{
Expand Down Expand Up @@ -989,8 +989,11 @@ func (b formMessageBuilder) initFormValues() {
} else {
b.FormValues["msgisreply"] = "False"
}

b.FormValues["msgsender"] = b.FormsMgr.config.MyCall
for _, key := range []string{"msgsender"} {
if _, ok := b.FormValues[key]; !ok {
b.FormValues[key] = b.FormsMgr.config.MyCall
}
}

// some defaults that we can't set yet. Winlink doesn't seem to care about these
// Set only if they're not set by form values.
Expand All @@ -1004,8 +1007,12 @@ func (b formMessageBuilder) initFormValues() {
b.FormValues[key] = "False"
}
}
if _, ok := b.FormValues["msgseqnum"]; !ok {
b.FormValues["msgseqnum"] = "0"

//TODO: Implement sequences
for _, key := range []string{"msgseqnum"} {
if _, ok := b.FormValues[key]; !ok {
b.FormValues[key] = "0"
}
}
}

Expand All @@ -1016,65 +1023,112 @@ func (b formMessageBuilder) scanTmplBuildMessage(tmplPath string) (MessageForm,
}
defer infile.Close()

placeholderRegEx := regexp.MustCompile(`<[vV][aA][rR]\s+(\w+)\s*>`)
placeholderRegEx := regexp.MustCompile(`(?i)<Var\s+(\w+)\s*>`)
scanner := bufio.NewScanner(infile)

var msgForm MessageForm
var inBody bool
for scanner.Scan() {
lineTmpl := scanner.Text()

// Insertion tags
lineTmpl = b.replaceInsertionTags(lineTmpl)

// Variables
lineTmpl = fillPlaceholders(lineTmpl, placeholderRegEx, b.FormValues)
lineTmpl = strings.ReplaceAll(lineTmpl, "<MsgSender>", b.FormsMgr.config.MyCall)
lineTmpl = strings.ReplaceAll(lineTmpl, "<ProgramVersion>", "Pat "+b.FormsMgr.config.AppVersion)
if strings.HasPrefix(lineTmpl, "Form:") {
continue
}
if strings.HasPrefix(lineTmpl, "ReplyTemplate:") {
continue
}
if strings.HasPrefix(lineTmpl, "Msg:") {
lineTmpl = strings.TrimSpace(strings.TrimPrefix(lineTmpl, "Msg:"))
inBody = true
}

// Prompts (mostly found in text templates)
if b.Interactive {
matches := placeholderRegEx.FindAllStringSubmatch(lineTmpl, -1)
fmt.Println(lineTmpl)
for i := range matches {
varName := matches[i][1]
varNameLower := strings.ToLower(varName)
if b.FormValues[varNameLower] != "" {
continue
lineTmpl = promptAsks(lineTmpl, func(a Ask) string {
//TODO: Handle a.Multiline as we do message body
fmt.Printf(a.Prompt + " ")
return b.FormsMgr.config.LineReader()
})
lineTmpl = promptSelects(lineTmpl, func(s Select) Option {
for {
fmt.Println(s.Prompt)
for i, opt := range s.Options {
fmt.Printf(" %d\t%s\n", i, opt.Item)
}
fmt.Printf("select 0-%d: ", len(s.Options)-1)
idx, err := strconv.Atoi(b.FormsMgr.config.LineReader())
if err == nil && idx < len(s.Options) {
return s.Options[idx]
}
}
fmt.Print(varName + ": ")
b.FormValues[varNameLower] = "blank"
val := b.FormsMgr.config.LineReader()
if val != "" {
b.FormValues[varNameLower] = val
}
}
})
// Fallback prompt for undefined form variables.
// Typically these are defined by the associated HTML form, but since
// this is CLI land we'll just prompt for the variable value.
lineTmpl = promptVars(lineTmpl, func(key string) string {
fmt.Printf("%s: ", key)
value := b.FormsMgr.config.LineReader()
b.FormValues[strings.ToLower(key)] = value
return value
})
}

lineTmpl = fillPlaceholders(lineTmpl, placeholderRegEx, b.FormValues)
switch key, value, _ := strings.Cut(lineTmpl, ":"); key {
case "Subject":
if inBody {
msgForm.Body += lineTmpl + "\n"
continue // No control fields in body
}

// Control fields
switch key, value, _ := strings.Cut(lineTmpl, ":"); textproto.CanonicalMIMEHeaderKey(key) {
case "Msg":
// The message body starts here. No more control fields after this.
msgForm.Body += value
inBody = true
case "Form", "ReplyTemplate":
// Handled elsewhere
continue
case "Def", "Define":
// Def: variable=value – Define the value of a variable.
key, value, ok := strings.Cut(value, "=")
if !ok {
debug.Printf("Def: without key-value pair: %q", value)
continue
}
key, value = strings.ToLower(strings.TrimSpace(key)), strings.TrimSpace(value)
b.FormValues[key] = value
debug.Printf("Defined %q=%q", key, value)
case "Subject", "Subj":
// Set the subject of the message
msgForm.Subject = strings.TrimSpace(value)
case "To":
// Specify to whom the message is being sent
msgForm.To = strings.TrimSpace(value)
case "Cc":
// Specify carbon copy addresses
msgForm.Cc = strings.TrimSpace(value)
case "Readonly":
// Yes/No – Specify whether user can edit.
// TODO: Disable editing of body in composer?
case "Seqinc":
//TODO: Handle sequences
default:
if inBody {
msgForm.Body += lineTmpl + "\n"
} else {
if strings.TrimSpace(lineTmpl) != "" {
log.Printf("skipping unknown template line: '%s'", lineTmpl)
}
}
}
return msgForm, nil
}

func (b formMessageBuilder) replaceInsertionTags(str string) string {
const tagStart, tagEnd = '<', '>'
m := map[string]string{
"Callsign": b.FormsMgr.config.MyCall,
"ProgramVersion": "Pat " + b.FormsMgr.config.AppVersion,
"SeqNum": b.FormValues["msgseqnum"], // TODO: Not a good idea since insertions are handled before vars.
}
for k, v := range m {
k = string(tagStart) + k + string(tagEnd)
str = strings.ReplaceAll(str, k, v)
}
return str
}

func xmlEscape(s string) string {
var buf bytes.Buffer
if err := xml.EscapeText(&buf, []byte(s)); err != nil {
Expand Down
72 changes: 72 additions & 0 deletions internal/forms/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package forms

import (
"regexp"
"strings"
)

type Select struct {
Prompt string
Options []Option
}

type Ask struct {
Prompt string
Multiline bool
}

type Option struct {
Item string
Value string
}

func promptAsks(str string, promptFn func(Ask) string) string {
re := regexp.MustCompile(`(?i)<Ask\s+([^,]+)(,[^>]+)?>`)
for {
tokens := re.FindAllStringSubmatch(str, -1)
if len(tokens) == 0 {
return str
}
replace, prompt, options := tokens[0][0], tokens[0][1], strings.TrimPrefix(tokens[0][2], ",")
a := Ask{Prompt: prompt, Multiline: strings.EqualFold(options, "MU")}
ans := promptFn(a)
str = strings.Replace(str, replace, ans, 1)
}
}

func promptSelects(str string, promptFn func(Select) Option) string {
re := regexp.MustCompile(`(?i)<Select\s+([^,]+)(,[^>]+)?>`)
for {
tokens := re.FindAllStringSubmatch(str, -1)
if len(tokens) == 0 {
return str
}
replace, prompt, options := tokens[0][0], tokens[0][1], strings.Split(strings.TrimPrefix(tokens[0][2], ","), ",")
s := Select{Prompt: prompt}
for _, opt := range options {
item, value, ok := strings.Cut(opt, "=")
if !ok {
value = item
}
s.Options = append(s.Options, Option{Item: item, Value: value})
}
ans := promptFn(s)
str = strings.Replace(str, replace, ans.Value, 1)
}
}

func promptVars(str string, promptFn func(string) string) string {
re := regexp.MustCompile(`(?i)<Var\s+(\w+)\s*>`)
for {
tokens := re.FindAllStringSubmatch(str, -1)
if len(tokens) == 0 {
return str
}
replace, key := tokens[0][0], tokens[0][1]
ans := promptFn(key)
if ans == "" {
ans = "blank"
}
str = strings.Replace(str, replace, ans, 1)
}
}
37 changes: 37 additions & 0 deletions internal/forms/prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package forms

import (
"fmt"
"testing"
)

func TestReplaceSelect(t *testing.T) {
tests := []struct {
In, Expect string
Answer func(Select) Option
}{
{
In: "",
Expect: "",
Answer: nil,
},
{
In: "foobar",
Expect: "foobar",
Answer: nil,
},
{
In: `Subj: //WL2K <Select Prioritet:,Routine=R/,Priority=P/,Immediate=O/,Flash=Z/> <Callsign>/<SeqNum> - <Var Subject>`,
Expect: `Subj: //WL2K R/ <Callsign>/<SeqNum> - <Var Subject>`,
Answer: func(s Select) Option { return s.Options[0] },
},
}
for i, tt := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
got := promptSelects(tt.In, tt.Answer)
if got != tt.Expect {
t.Errorf("Expected %q, got %q", tt.Expect, got)
}
})
}
}

0 comments on commit c409d48

Please sign in to comment.