|
@@ -40,6 +40,26 @@ func (ctx remarkContext) configName() string {
|
|
|
// underscores only, so ordinary braces in a remark are left untouched.
|
|
// underscores only, so ordinary braces in a remark are left untouched.
|
|
|
var remarkVarRe = regexp.MustCompile(`\{\{([A-Z_]+)\}\}`)
|
|
var remarkVarRe = regexp.MustCompile(`\{\{([A-Z_]+)\}\}`)
|
|
|
|
|
|
|
|
|
|
+// remarkToken is one {{TOKEN}} occurrence: its name and the byte range it spans
|
|
|
|
|
+// in the segment it was found in.
|
|
|
|
|
+type remarkToken struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ start int
|
|
|
|
|
+ end int
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// remarkTokens locates every {{TOKEN}} in seg. Both the template-level filter and
|
|
|
|
|
+// the value-level expansion walk a segment through this, so they share one notion
|
|
|
|
|
+// of where a token begins and ends and what the literal text between two of them is.
|
|
|
|
|
+func remarkTokens(seg string) []remarkToken {
|
|
|
|
|
+ locs := remarkVarRe.FindAllStringSubmatchIndex(seg, -1)
|
|
|
|
|
+ tokens := make([]remarkToken, len(locs))
|
|
|
|
|
+ for i, loc := range locs {
|
|
|
|
|
+ tokens[i] = remarkToken{name: seg[loc[2]:loc[3]], start: loc[0], end: loc[1]}
|
|
|
|
|
+ }
|
|
|
|
|
+ return tokens
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// unlimitedMark is the value the human-readable quota/expiry tokens render when
|
|
// unlimitedMark is the value the human-readable quota/expiry tokens render when
|
|
|
// the client has no limit. A segment built only around such a token carries no
|
|
// the client has no limit. A segment built only around such a token carries no
|
|
|
// information, so it is dropped rather than printed as "∞" (see expandRemarkVars).
|
|
// information, so it is dropped rather than printed as "∞" (see expandRemarkVars).
|
|
@@ -107,7 +127,9 @@ func translateUISingleBrackets(template string) string {
|
|
|
// value. Unknown tokens resolve to "" (never the literal text). The template is
|
|
// value. Unknown tokens resolve to "" (never the literal text). The template is
|
|
|
// split on "|" into segments: a segment whose only value is an unlimited quota
|
|
// split on "|" into segments: a segment whose only value is an unlimited quota
|
|
|
// or expiry (∞) drops out whole — decoration and separator included — so an
|
|
// or expiry (∞) drops out whole — decoration and separator included — so an
|
|
|
-// unlimited client gets "host" instead of "host|📊∞|⏳∞D".
|
|
|
|
|
|
|
+// unlimited client gets "host" instead of "host|📊∞|⏳∞D". Inside a surviving
|
|
|
|
|
+// segment expandSegment also elides a hyphen separator an empty token would
|
|
|
|
|
+// leave dangling.
|
|
|
func expandRemarkVars(template string, ctx remarkContext) string {
|
|
func expandRemarkVars(template string, ctx remarkContext) string {
|
|
|
template = translateUISingleBrackets(template)
|
|
template = translateUISingleBrackets(template)
|
|
|
if !strings.Contains(template, "{{") {
|
|
if !strings.Contains(template, "{{") {
|
|
@@ -129,18 +151,43 @@ func expandRemarkVars(template string, ctx remarkContext) string {
|
|
|
// — so it leaves no stray "|" separator or dangling decoration. A segment mixing,
|
|
// — so it leaves no stray "|" separator or dangling decoration. A segment mixing,
|
|
|
// say, {{EMAIL}} with {{TRAFFIC_LEFT}} is kept, and a pure-literal segment (no
|
|
// say, {{EMAIL}} with {{TRAFFIC_LEFT}} is kept, and a pure-literal segment (no
|
|
|
// tokens) is always kept.
|
|
// tokens) is always kept.
|
|
|
|
|
+//
|
|
|
|
|
+// A hyphen standing alone between two adjacent tokens is treated as their
|
|
|
|
|
+// separator and elided when no token before it has produced a value yet or when
|
|
|
|
|
+// the token after it resolves to nothing. "{{INBOUND}}-{{EMAIL}}" gives "john"
|
|
|
|
|
+// for an inbound with no remark, "🌐{{INBOUND}}-{{EMAIL}}" gives "🌐john" so
|
|
|
|
|
+// leading decoration does not keep the separator alive, and
|
|
|
|
|
+// "{{EMAIL}}-{{INBOUND}}-{{EMAIL}}" keeps a single separator when the middle
|
|
|
|
|
+// token is empty. A hyphen anywhere else in the segment is literal text and is
|
|
|
|
|
+// kept as written.
|
|
|
func expandSegment(seg string, ctx remarkContext) (string, bool) {
|
|
func expandSegment(seg string, ctx remarkContext) (string, bool) {
|
|
|
- hasToken, hasOtherValue := false, false
|
|
|
|
|
- out := remarkVarRe.ReplaceAllStringFunc(seg, func(m string) string {
|
|
|
|
|
- hasToken = true
|
|
|
|
|
- token := m[2 : len(m)-2]
|
|
|
|
|
- val := remarkVarValue(token, ctx)
|
|
|
|
|
- if val != "" && (!unlimitedDropTokens[token] || val != unlimitedMark) {
|
|
|
|
|
|
|
+ tokens := remarkTokens(seg)
|
|
|
|
|
+ hasToken, hasOtherValue := len(tokens) > 0, false
|
|
|
|
|
+ values := make([]string, len(tokens))
|
|
|
|
|
+ for i, tok := range tokens {
|
|
|
|
|
+ val := remarkVarValue(tok.name, ctx)
|
|
|
|
|
+ values[i] = val
|
|
|
|
|
+ if val != "" && (!unlimitedDropTokens[tok.name] || val != unlimitedMark) {
|
|
|
hasOtherValue = true
|
|
hasOtherValue = true
|
|
|
}
|
|
}
|
|
|
- return val
|
|
|
|
|
- })
|
|
|
|
|
- return out, hasToken && !hasOtherValue
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var result strings.Builder
|
|
|
|
|
+ start, wroteValue := 0, false
|
|
|
|
|
+ for i, tok := range tokens {
|
|
|
|
|
+ result.WriteString(seg[start:tok.start])
|
|
|
|
|
+ result.WriteString(values[i])
|
|
|
|
|
+ wroteValue = wroteValue || values[i] != ""
|
|
|
|
|
+ start = tok.end
|
|
|
|
|
+ if i+1 < len(tokens) {
|
|
|
|
|
+ between := seg[start:tokens[i+1].start]
|
|
|
|
|
+ if strings.TrimSpace(between) == "-" && (!wroteValue || values[i+1] == "") {
|
|
|
|
|
+ start = tokens[i+1].start
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ result.WriteString(seg[start:])
|
|
|
|
|
+ return result.String(), hasToken && !hasOtherValue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func remarkVarValue(token string, ctx remarkContext) string {
|
|
func remarkVarValue(token string, ctx remarkContext) string {
|
|
@@ -511,11 +558,17 @@ func filterRemarkTemplate(template string, remove map[string]bool) string {
|
|
|
return strings.Join(kept, "|")
|
|
return strings.Join(kept, "|")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// filterRemarkSegment drops whole token categories from one segment while it is
|
|
|
|
|
+// still a template, before any value is known. Literal text touching a removed
|
|
|
|
|
+// token goes with it and the surviving runs rejoin with a space, so filtering the
|
|
|
|
|
+// usage tokens out of "{{EMAIL}} 📊{{TRAFFIC_LEFT}}" leaves "{{EMAIL}}". This is
|
|
|
|
|
+// the template-level counterpart to expandSegment, which works one layer later on
|
|
|
|
|
+// tokens that survive here but resolve to an empty value.
|
|
|
func filterRemarkSegment(seg string, remove map[string]bool) string {
|
|
func filterRemarkSegment(seg string, remove map[string]bool) string {
|
|
|
- locs := remarkVarRe.FindAllStringSubmatchIndex(seg, -1)
|
|
|
|
|
|
|
+ tokens := remarkTokens(seg)
|
|
|
hasRemove := false
|
|
hasRemove := false
|
|
|
- for _, loc := range locs {
|
|
|
|
|
- if remove[seg[loc[2]:loc[3]]] {
|
|
|
|
|
|
|
+ for _, tok := range tokens {
|
|
|
|
|
+ if remove[tok.name] {
|
|
|
hasRemove = true
|
|
hasRemove = true
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -525,28 +578,28 @@ func filterRemarkSegment(seg string, remove map[string]bool) string {
|
|
|
}
|
|
}
|
|
|
runs := make([]string, 0, 2)
|
|
runs := make([]string, 0, 2)
|
|
|
runStart, leftRemoved := 0, false
|
|
runStart, leftRemoved := 0, false
|
|
|
- for _, loc := range locs {
|
|
|
|
|
- if !remove[seg[loc[2]:loc[3]]] {
|
|
|
|
|
|
|
+ for _, tok := range tokens {
|
|
|
|
|
+ if !remove[tok.name] {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- runs = appendKeptRun(runs, seg[runStart:loc[0]], leftRemoved, true)
|
|
|
|
|
- runStart, leftRemoved = loc[1], true
|
|
|
|
|
|
|
+ runs = appendKeptRun(runs, seg[runStart:tok.start], leftRemoved, true)
|
|
|
|
|
+ runStart, leftRemoved = tok.end, true
|
|
|
}
|
|
}
|
|
|
runs = appendKeptRun(runs, seg[runStart:], leftRemoved, false)
|
|
runs = appendKeptRun(runs, seg[runStart:], leftRemoved, false)
|
|
|
return strings.Join(runs, " ")
|
|
return strings.Join(runs, " ")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func appendKeptRun(runs []string, run string, leftRemoved, rightRemoved bool) []string {
|
|
func appendKeptRun(runs []string, run string, leftRemoved, rightRemoved bool) []string {
|
|
|
- locs := remarkVarRe.FindAllStringSubmatchIndex(run, -1)
|
|
|
|
|
- if len(locs) == 0 {
|
|
|
|
|
|
|
+ tokens := remarkTokens(run)
|
|
|
|
|
+ if len(tokens) == 0 {
|
|
|
return runs
|
|
return runs
|
|
|
}
|
|
}
|
|
|
start, end := 0, len(run)
|
|
start, end := 0, len(run)
|
|
|
if leftRemoved {
|
|
if leftRemoved {
|
|
|
- start = locs[0][0]
|
|
|
|
|
|
|
+ start = tokens[0].start
|
|
|
}
|
|
}
|
|
|
if rightRemoved {
|
|
if rightRemoved {
|
|
|
- end = locs[len(locs)-1][1]
|
|
|
|
|
|
|
+ end = tokens[len(tokens)-1].end
|
|
|
}
|
|
}
|
|
|
if frag := strings.TrimSpace(run[start:end]); frag != "" {
|
|
if frag := strings.TrimSpace(run[start:end]); frag != "" {
|
|
|
runs = append(runs, frag)
|
|
runs = append(runs, frag)
|