diff --git a/formatter.go b/formatter.go index 7a1297c..320d901 100644 --- a/formatter.go +++ b/formatter.go @@ -9,6 +9,12 @@ import ( const argumentFormatSeparator = ":" const bytesPerArgDefault = 16 +type processingState int + +const charAnalyzeState processingState = 1 +const segmentBeginDetectionState processingState = 2 +const segmentEndDetectionState processingState = 3 + // Format /* Func that makes string formatting from template * It differs from above function only by generic interface that allow to use only primitive data types: @@ -38,104 +44,205 @@ func Format(template string, args ...any) string { formattedStr := &strings.Builder{} argsLen := bytesPerArgDefault * len(args) formattedStr.Grow(templateLen + argsLen + 1) - j := -1 //nolint:ineffassign + j := -1 //nolint:ineffassign + i := start // ??? + repeatingOpenBrackets := 0 + repeatingOpenBracketsCollected := false + repeatingCloseBrackets := 0 + prevCloseBracketIndex := 0 + copyWithBrackets := false - nestedBrackets := false formattedStr.WriteString(template[:start]) - for i := start; i < templateLen; i++ { - if template[i] == '{' { - // possibly it is a template placeholder - if i == templateLen-1 { - // if we gave { at the end of line i.e. -> type serviceHealth struct {, - // without this write we got type serviceHealth struct - formattedStr.WriteByte('{') - break + state := charAnalyzeState + for { + // infinite loop, state changes on template string symbols processing, initially + // we have charAnalyzeState state + if state == charAnalyzeState { + // this state is a space between segments + // 1.1 remember j to WriteStr from j to i + if j < 0 { + j = i } - // considering in 2 phases - {{ }} - if template[i+1] == '{' { - formattedStr.WriteByte('{') - continue + + if template[i] == '{' && i <= templateLen-2 { + formattedStr.WriteString(template[j:i]) + // 1.2 using j to remember a start of possible segment + j = i + state = segmentBeginDetectionState + repeatingOpenBracketsCollected = false + repeatingOpenBrackets = 1 } - // find end of placeholder - // process empty pair - {} - if template[i+1] == '}' { - i++ - formattedStr.WriteString("{}") - continue + + if i == templateLen-1 { + state = segmentEndDetectionState } - // process non-empty placeholder - j = i + 2 - for { - if j >= templateLen { - break + } else { + if state == segmentBeginDetectionState { + // segment could be complicated: + if template[i] == '{' { + // we are not dealing with segment, if there are symbols between { and { + if template[i-1] != '{' { + state = charAnalyzeState + // skip increment i, process in charAnalyzeState + continue + } + if !repeatingOpenBracketsCollected { + repeatingOpenBrackets++ + } + } else { + repeatingOpenBracketsCollected = true } - - if template[j] == '{' { - // multiple nested curly brackets ... - nestedBrackets = true - formattedStr.WriteString(template[i:j]) - i = j + // 1. JSON object, therefore we skip it + // 2. multiple nested seg {{{ and non-equal or equal number of closing brackets + if template[i] == '}' { + state = segmentEndDetectionState + repeatingCloseBrackets = 1 + prevCloseBracketIndex = i } - if template[j] == '}' { - break + // we started to detect, but not finished yet + if i == templateLen-1 { + state = segmentEndDetectionState } - - j++ - } - // double curly brackets processed here, convert {{N}} -> {N} - // so we catch here {{N} - if j+1 < templateLen && template[j+1] == '}' && template[i-1] == '{' { - formattedStr.WriteString(template[i+1 : j+1]) - i = j + 1 } else { - argNumberStr := template[i+1 : j] - // is here we should support formatting ? - var argNumber int - var err error - var argFormatOptions string - if len(argNumberStr) == 1 { - // this calculation makes work a little faster than AtoI - argNumber = int(argNumberStr[0] - '0') - } else { - argNumber = -1 - // Here we are going to process argument either with additional formatting or not - // i.e. 0 for arg without formatting && 0:format for an argument wit formatting - // todo(UMV): we could format json or yaml here ... - formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator) - // formatOptionIndex can't be == 0, because 0 is a position of arg number - if formatOptionIndex > 0 { - // trimmed was down later due to we could format list with space separator - argFormatOptions = argNumberStr[formatOptionIndex+1:] - argNumberStrPart := argNumberStr[:formatOptionIndex] - argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " ")) - if err == nil { - argNumberStr = argNumberStrPart + if state == segmentEndDetectionState { + if template[i] != '}' || // end of the segment + i == templateLen-1 { // end of the line + if i == templateLen-1 { + // we didn't process close bracket symbol in previous states, or in this state in diff branch + if template[i] == '}' { + if prevCloseBracketIndex != i { + repeatingCloseBrackets++ + } + } } - // make formatting option str for further pass to an argument - } - // - if argNumber < 0 { - argNumber, err = strconv.Atoi(argNumberStr) - } - } - if (err == nil || (argFormatOptions != "" && !nestedBrackets)) && - len(args) > argNumber { - // get number from placeholder - strVal := getItemAsStr(&args[argNumber], &argFormatOptions) - formattedStr.WriteString(strVal) - } else { - formattedStr.WriteString(template[i:j]) - if j < templateLen-1 { - formattedStr.WriteByte(template[j]) + copyWithBrackets = false + delta := repeatingOpenBrackets - repeatingCloseBrackets + // 1. Handle brackets before parts with equal number of brackets + if delta > 0 { + // Write { delta times + for z := 0; z < delta; z++ { + formattedStr.WriteByte('{') + } + j += delta + } + // 2. Handle segment {..{arg}..} with equal number of brackets + // 2.1 Multiple curly brackets handler + isEven := (repeatingOpenBrackets % 2) == 0 + // single - {argNumberStr} handles by replace argNumber by data from list. double {{argNumberStr}} produces {argNumberStr} + // triple - prof + segmentPrecedingBrackets := repeatingOpenBrackets / 2 + + if !isEven { + segmentPrecedingBrackets = (repeatingOpenBrackets - 1) / 2 + } + + for z := 0; z < segmentPrecedingBrackets; z++ { + formattedStr.WriteByte('{') + } + + startIndex := j + repeatingOpenBrackets + endIndex := i - repeatingCloseBrackets + // don't like this, this is a shit + if i == templateLen-1 { + // we add endIndex +1 because selection at the mid of template line assumes that + // processes segment at the next symbol i+1, but at the end of line we can't process i+1 + // therefore we manipulate selection indexes but ONLY in case when segment at the end of template + if !(repeatingOpenBrackets > 0 && template[templateLen-1] != '}') { + endIndex += 1 + } + } + argNumberStr := template[startIndex:endIndex] + // 2.2 Segment formatting + if !isEven { + j += repeatingOpenBrackets - 1 + argNumber := -1 + var err error + var argFormatOptions string + if len(argNumberStr) == 1 { + // this calculation makes work a little faster than AtoI + argNumber = int(argNumberStr[0] - '0') + //rawWrite = false + } else { + argNumber = -1 + // Here we are going to process argument either with additional formatting or not + // i.e. 0 for arg without formatting && 0:format for an argument wit formatting + // todo(UMV): we could format json or yaml here ... + formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator) + // formatOptionIndex can't be == 0, because 0 is a position of arg number + if formatOptionIndex > 0 { + // trimmed was down later due to we could format list with space separator + argFormatOptions = argNumberStr[formatOptionIndex+1:] + argNumberStrPart := argNumberStr[:formatOptionIndex] + argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " ")) + if err == nil { + argNumberStr = argNumberStrPart + //rawWrite = false + } + + // make formatting option str for further pass to an argument + } + if argNumber < 0 { + argNumber, err = strconv.Atoi(argNumberStr) + if err == nil { + //rawWrite = false + } + } + } + + if (err == nil || (argFormatOptions != "" && !repeatingOpenBracketsCollected)) && + len(args) > argNumber { + // get number from placeholder + strVal := getItemAsStr(&args[argNumber], &argFormatOptions) + formattedStr.WriteString(strVal) + } else { + copyWithBrackets = true + if i < templateLen-1 { + formattedStr.WriteString(template[j:i]) + } else { + // if i is the last symbol in template line, we should take i+1 + formattedStr.WriteString(template[j : i+1]) //template + } + + } + } else { + formattedStr.WriteString(argNumberStr) + } + + for z := 0; z < segmentPrecedingBrackets; z++ { + formattedStr.WriteByte('}') + } + + // 3. Handle brackets after segment + if !copyWithBrackets { + for z := 0; z < delta*-1; z++ { + formattedStr.WriteByte('}') + } + } + + state = charAnalyzeState + if i == templateLen-1 { + // this is for writing last symbol that follows after segment at the end of template + if endIndex < templateLen-1 && template[templateLen-1] != '}' { + formattedStr.WriteByte(template[templateLen-1]) + } + break + } else { + j = i + } + repeatingOpenBrackets = 0 + repeatingCloseBrackets = 0 + } else { + repeatingCloseBrackets++ + prevCloseBracketIndex = i } } - i = j } - } else { - j = i //nolint:ineffassign - formattedStr.WriteByte(template[i]) + } + // sometimes we are using continue to move to another state within current i value + if i < templateLen-1 { + i++ } } @@ -143,7 +250,7 @@ func Format(template string, args ...any) string { } // FormatComplex -/* Function that format text using more complex templates contains string literals i.e "Hello {username} here is our application {appname} +/* Function that format text using more complex templates contains string literals i.e "Hello {username} here is our application {appname}" * Parameters * - template - string that contains template * - args - values (dictionary: string key - any value) that are using for formatting with template @@ -163,91 +270,208 @@ func FormatComplex(template string, args map[string]any) string { formattedStr := &strings.Builder{} argsLen := bytesPerArgDefault * len(args) formattedStr.Grow(templateLen + argsLen + 1) - j := -1 //nolint:ineffassign - nestedBrackets := false + j := -1 //nolint:ineffassign + i := start // ??? + repeatingOpenBrackets := 0 + repeatingOpenBracketsCollected := false + repeatingCloseBrackets := 0 + prevCloseBracketIndex := 0 + copyWithBrackets := false + formattedStr.WriteString(template[:start]) - for i := start; i < templateLen; i++ { - if template[i] == '{' { - // possibly it is a template placeholder - if i == templateLen-1 { - // if we gave { at the end of line i.e. -> type serviceHealth struct {, - // without this write we got type serviceHealth struct - formattedStr.WriteByte('{') - break + state := charAnalyzeState + for { + // infinite loop, state changes on template string symbols processing, initially + // we have charAnalyzeState state + if state == charAnalyzeState { + // this state is a space between segments + // 1.1 remember j to WriteStr from j to i + if j < 0 { + j = i } - if template[i+1] == '{' { - formattedStr.WriteByte('{') - continue + if template[i] == '{' && i <= templateLen-2 { + formattedStr.WriteString(template[j:i]) + // 1.2 using j to remember a start of possible segment + j = i + state = segmentBeginDetectionState + repeatingOpenBracketsCollected = false + repeatingOpenBrackets = 1 } - // find end of placeholder - // process empty pair - {} - if template[i+1] == '}' { - i++ - formattedStr.WriteString("{}") - continue - } - // process non-empty placeholder - // find end of placeholder - j = i + 2 - for { - if j >= templateLen { - break + if i == templateLen-1 { + state = segmentEndDetectionState + } + } else { + if state == segmentBeginDetectionState { + // segment could be complicated: + if template[i] == '{' { + // we are not dealing with segment, if there are symbols between { and { + if template[i-1] != '{' { + state = charAnalyzeState + // skip increment i, process in charAnalyzeState + continue + } + if !repeatingOpenBracketsCollected { + repeatingOpenBrackets++ + } + } else { + repeatingOpenBracketsCollected = true } - if template[j] == '{' { - // multiple nested curly brackets ... - nestedBrackets = true - formattedStr.WriteString(template[i:j]) - i = j + // 1. JSON object, therefore we skip it + // 2. multiple nested seg {{{ and non-equal or equal number of closing brackets + if template[i] == '}' { + state = segmentEndDetectionState + repeatingCloseBrackets = 1 + prevCloseBracketIndex = i } - if template[j] == '}' { - break + + // we started to detect, but not finished yet + if i == templateLen-1 { + state = segmentEndDetectionState } - j++ - } - // double curly brackets processed here, convert {{N}} -> {N} - // so we catch here {{N} - if j+1 < templateLen && template[j+1] == '}' { - formattedStr.WriteString(template[i+1 : j+1]) - i = j + 1 } else { - var argFormatOptions string - argNumberStr := template[i+1 : j] - arg, ok := args[argNumberStr] - if !ok { - formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator) - if formatOptionIndex >= 0 { - // argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ") - argFormatOptions = argNumberStr[formatOptionIndex+1:] - argNumberStr = strings.Trim(argNumberStr[:formatOptionIndex], " ") - } + if state == segmentEndDetectionState { + if template[i] != '}' || // end of the segment + i == templateLen-1 { // end of the line + if i == templateLen-1 { + // we didn't process close bracket symbol in previous states, or in this state in diff branch + if template[i] == '}' { + if prevCloseBracketIndex != i { + repeatingCloseBrackets++ + } + } + } - arg, ok = args[argNumberStr] - } - if ok || (argFormatOptions != "" && !nestedBrackets) { - // get number from placeholder - strVal := "" - if arg != nil { - strVal = getItemAsStr(&arg, &argFormatOptions) - } else { - formattedStr.WriteString(template[i:j]) - if j < templateLen-1 { - formattedStr.WriteByte(template[j]) + copyWithBrackets = false + delta := repeatingOpenBrackets - repeatingCloseBrackets + // 1. Handle brackets before parts with equal number of brackets + if delta > 0 { + // Write { delta times + for z := 0; z < delta; z++ { + formattedStr.WriteByte('{') + } + j += delta } - } - formattedStr.WriteString(strVal) - } else { - formattedStr.WriteString(template[i:j]) - if j < templateLen-1 { - formattedStr.WriteByte(template[j]) + // 2. Handle segment {..{arg}..} with equal number of brackets + // 2.1 Multiple curly brackets handler + isEven := (repeatingOpenBrackets % 2) == 0 + // single - {argNumberStr} handles by replace argNumber by data from list. double {{argNumberStr}} produces {argNumberStr} + // triple - prof + segmentPrecedingBrackets := repeatingOpenBrackets / 2 + + if !isEven { + segmentPrecedingBrackets = (repeatingOpenBrackets - 1) / 2 + } + + for z := 0; z < segmentPrecedingBrackets; z++ { + formattedStr.WriteByte('{') + } + + startIndex := j + repeatingOpenBrackets + endIndex := i - repeatingCloseBrackets + // don't like this, this is a shit + if i == templateLen-1 { + // we add endIndex +1 because selection at the mid of template line assumes that + // processes segment at the next symbol i+1, but at the end of line we can't process i+1 + // therefore we manipulate selection indexes but ONLY in case when segment at the end of template + if !(repeatingOpenBrackets > 0 && template[templateLen-1] != '}') { + endIndex += 1 + } + } + argKeyStr := template[startIndex:endIndex] + argKey := argKeyStr + + // 2.2 Segment formatting + if !isEven { + j += repeatingOpenBrackets - 1 + var argFormatOptions string + // var argNumberStrPart string + + // Here we are going to process argument either with additional formatting or not + // i.e. 0 for arg without formatting && 0:format for an argument wit formatting + // todo(UMV): we could format json or yaml here ... + formatOptionIndex := strings.Index(argKeyStr, argumentFormatSeparator) + // formatOptionIndex can't be == 0, because 0 is a position of arg number + if formatOptionIndex > 0 { + // trimmed was down later due to we could format list with space separator + argFormatOptions = argKeyStr[formatOptionIndex+1:] + argKey = argKeyStr[:formatOptionIndex] + } + + arg, ok := args[argKey] + if !ok { + formatOptionIndex = strings.Index(argKeyStr, argumentFormatSeparator) + if formatOptionIndex >= 0 { + // argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ") + argFormatOptions = argKeyStr[formatOptionIndex+1:] + argKey = strings.Trim(argKey[:formatOptionIndex], " ") + } + + arg, ok = args[argKey] + } + if ok || (argFormatOptions != "" && !!repeatingOpenBracketsCollected) { + // get number from placeholder + strVal := "" + if arg != nil { + strVal = getItemAsStr(&arg, &argFormatOptions) + } else { + copyWithBrackets = true + if i < templateLen-1 { + formattedStr.WriteString(template[j:i]) + } else { + // if i is the last symbol in template line, we should take i+1 + formattedStr.WriteString(template[j : i+1]) //template + } + } + formattedStr.WriteString(strVal) + } else { + copyWithBrackets = true + if i < templateLen-1 { + formattedStr.WriteString(template[j:i]) + } else { + // if i is the last symbol in template line, we should take i+1 + formattedStr.WriteString(template[j : i+1]) //template + } + } + + } else { + formattedStr.WriteString(argKeyStr) + } + + for z := 0; z < segmentPrecedingBrackets; z++ { + formattedStr.WriteByte('}') + } + + // 3. Handle brackets after segment + if !copyWithBrackets { + for z := 0; z < delta*-1; z++ { + formattedStr.WriteByte('}') + } + } + + state = charAnalyzeState + if i == templateLen-1 { + // this is for writing last symbol that follows after segment at the end of template + if endIndex < templateLen-1 && template[templateLen-1] != '}' { + formattedStr.WriteByte(template[templateLen-1]) + } + break + } else { + j = i + } + repeatingOpenBrackets = 0 + repeatingCloseBrackets = 0 + } else { + repeatingCloseBrackets++ + prevCloseBracketIndex = i } } - i = j } - } else { - j = i //nolint:ineffassign - formattedStr.WriteByte(template[i]) + } + // sometimes we are using continue to move to another state within current i value + if i < templateLen-1 { + i++ } } diff --git a/formatter_test.go b/formatter_test.go index 1185453..499918f 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -55,7 +55,7 @@ func TestFormat(t *testing.T) { }`, }, "multiple nested curly brackets": { - template: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" {0}, ` + + template: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" , ` + `"Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}` + `, "End": true}}}`, args: []any{""}, @@ -101,6 +101,11 @@ func TestFormat(t *testing.T) { args: []any{"s"}, expected: "At the end {0}", }, + "quadro curly brackets in the middle": { + template: "Not at the end {{{{0}}}}, in the middle", + args: []any{"s"}, + expected: "Not at the end {{0}}, in the middle", + }, "struct arg": { template: "Example is: {0}", args: []any{ @@ -150,6 +155,36 @@ func TestFormat(t *testing.T) { args: []any{}, expected: "in the middle - { at the end - nothing", }, + "code line with interface": { + template: "[]any{singleValue}", + args: []any{}, + expected: "[]any{singleValue}", + }, + "code line with interface with val": { + template: "[]any{{{0}}}", + args: []any{"\"USSR!\""}, + expected: "[]any{\"USSR!\"}", + }, + "2-symb str": { + template: "a}", + args: []any{}, + expected: "a}", + }, + "one symb segment": { + template: "{x}", + args: []any{}, + expected: "{x}", + }, + "one symb template": { + template: "{", + args: []any{}, + expected: "{", + }, + "one symb template2": { + template: "}", + args: []any{}, + expected: "}", + }, } { t.Run(name, func(t *testing.T) { assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...)) @@ -326,6 +361,36 @@ func TestFormatComplex(t *testing.T) { args: map[string]any{}, expected: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive", }, + "code line with interface": { + template: "[]any{singleValue}", + args: map[string]any{}, + expected: "[]any{singleValue}", + }, + "code line with interface with val": { + template: "[]any{{{val}}}", + args: map[string]any{"val": "\"USSR!\""}, + expected: "[]any{\"USSR!\"}", + }, + "2-symb str": { + template: "a}", + args: map[string]any{}, + expected: "a}", + }, + "one symb segment": { + template: "{x}", + args: map[string]any{}, + expected: "{x}", + }, + "one symb template": { + template: "{", + args: map[string]any{}, + expected: "{", + }, + "one symb template2": { + template: "}", + args: map[string]any{}, + expected: "}", + }, } { t.Run(name, func(t *testing.T) { assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args))