1package govalidator
2
3import (
4	"errors"
5	"fmt"
6	"html"
7	"math"
8	"path"
9	"regexp"
10	"strings"
11	"unicode"
12	"unicode/utf8"
13)
14
15// Contains check if the string contains the substring.
16func Contains(str, substring string) bool {
17	return strings.Contains(str, substring)
18}
19
20// Matches check if string matches the pattern (pattern is regular expression)
21// In case of error return false
22func Matches(str, pattern string) bool {
23	match, _ := regexp.MatchString(pattern, str)
24	return match
25}
26
27// LeftTrim trim characters from the left-side of the input.
28// If second argument is empty, it's will be remove leading spaces.
29func LeftTrim(str, chars string) string {
30	if chars == "" {
31		return strings.TrimLeftFunc(str, unicode.IsSpace)
32	}
33	r, _ := regexp.Compile("^[" + chars + "]+")
34	return r.ReplaceAllString(str, "")
35}
36
37// RightTrim trim characters from the right-side of the input.
38// If second argument is empty, it's will be remove spaces.
39func RightTrim(str, chars string) string {
40	if chars == "" {
41		return strings.TrimRightFunc(str, unicode.IsSpace)
42	}
43	r, _ := regexp.Compile("[" + chars + "]+$")
44	return r.ReplaceAllString(str, "")
45}
46
47// Trim trim characters from both sides of the input.
48// If second argument is empty, it's will be remove spaces.
49func Trim(str, chars string) string {
50	return LeftTrim(RightTrim(str, chars), chars)
51}
52
53// WhiteList remove characters that do not appear in the whitelist.
54func WhiteList(str, chars string) string {
55	pattern := "[^" + chars + "]+"
56	r, _ := regexp.Compile(pattern)
57	return r.ReplaceAllString(str, "")
58}
59
60// BlackList remove characters that appear in the blacklist.
61func BlackList(str, chars string) string {
62	pattern := "[" + chars + "]+"
63	r, _ := regexp.Compile(pattern)
64	return r.ReplaceAllString(str, "")
65}
66
67// StripLow remove characters with a numerical value < 32 and 127, mostly control characters.
68// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
69func StripLow(str string, keepNewLines bool) string {
70	chars := ""
71	if keepNewLines {
72		chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
73	} else {
74		chars = "\x00-\x1F\x7F"
75	}
76	return BlackList(str, chars)
77}
78
79// ReplacePattern replace regular expression pattern in string
80func ReplacePattern(str, pattern, replace string) string {
81	r, _ := regexp.Compile(pattern)
82	return r.ReplaceAllString(str, replace)
83}
84
85// Escape replace <, >, & and " with HTML entities.
86var Escape = html.EscapeString
87
88func addSegment(inrune, segment []rune) []rune {
89	if len(segment) == 0 {
90		return inrune
91	}
92	if len(inrune) != 0 {
93		inrune = append(inrune, '_')
94	}
95	inrune = append(inrune, segment...)
96	return inrune
97}
98
99// UnderscoreToCamelCase converts from underscore separated form to camel case form.
100// Ex.: my_func => MyFunc
101func UnderscoreToCamelCase(s string) string {
102	return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
103}
104
105// CamelCaseToUnderscore converts from camel case form to underscore separated form.
106// Ex.: MyFunc => my_func
107func CamelCaseToUnderscore(str string) string {
108	var output []rune
109	var segment []rune
110	for _, r := range str {
111
112		// not treat number as separate segment
113		if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
114			output = addSegment(output, segment)
115			segment = nil
116		}
117		segment = append(segment, unicode.ToLower(r))
118	}
119	output = addSegment(output, segment)
120	return string(output)
121}
122
123// Reverse return reversed string
124func Reverse(s string) string {
125	r := []rune(s)
126	for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
127		r[i], r[j] = r[j], r[i]
128	}
129	return string(r)
130}
131
132// GetLines split string by "\n" and return array of lines
133func GetLines(s string) []string {
134	return strings.Split(s, "\n")
135}
136
137// GetLine return specified line of multiline string
138func GetLine(s string, index int) (string, error) {
139	lines := GetLines(s)
140	if index < 0 || index >= len(lines) {
141		return "", errors.New("line index out of bounds")
142	}
143	return lines[index], nil
144}
145
146// RemoveTags remove all tags from HTML string
147func RemoveTags(s string) string {
148	return ReplacePattern(s, "<[^>]*>", "")
149}
150
151// SafeFileName return safe string that can be used in file names
152func SafeFileName(str string) string {
153	name := strings.ToLower(str)
154	name = path.Clean(path.Base(name))
155	name = strings.Trim(name, " ")
156	separators, err := regexp.Compile(`[ &_=+:]`)
157	if err == nil {
158		name = separators.ReplaceAllString(name, "-")
159	}
160	legal, err := regexp.Compile(`[^[:alnum:]-.]`)
161	if err == nil {
162		name = legal.ReplaceAllString(name, "")
163	}
164	for strings.Contains(name, "--") {
165		name = strings.Replace(name, "--", "-", -1)
166	}
167	return name
168}
169
170// NormalizeEmail canonicalize an email address.
171// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
172// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
173// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
174// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
175// normalized to @gmail.com.
176func NormalizeEmail(str string) (string, error) {
177	if !IsEmail(str) {
178		return "", fmt.Errorf("%s is not an email", str)
179	}
180	parts := strings.Split(str, "@")
181	parts[0] = strings.ToLower(parts[0])
182	parts[1] = strings.ToLower(parts[1])
183	if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
184		parts[1] = "gmail.com"
185		parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
186	}
187	return strings.Join(parts, "@"), nil
188}
189
190// Truncate a string to the closest length without breaking words.
191func Truncate(str string, length int, ending string) string {
192	var aftstr, befstr string
193	if len(str) > length {
194		words := strings.Fields(str)
195		before, present := 0, 0
196		for i := range words {
197			befstr = aftstr
198			before = present
199			aftstr = aftstr + words[i] + " "
200			present = len(aftstr)
201			if present > length && i != 0 {
202				if (length - before) < (present - length) {
203					return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
204				}
205				return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
206			}
207		}
208	}
209
210	return str
211}
212
213// PadLeft pad left side of string if size of string is less then indicated pad length
214func PadLeft(str string, padStr string, padLen int) string {
215	return buildPadStr(str, padStr, padLen, true, false)
216}
217
218// PadRight pad right side of string if size of string is less then indicated pad length
219func PadRight(str string, padStr string, padLen int) string {
220	return buildPadStr(str, padStr, padLen, false, true)
221}
222
223// PadBoth pad sides of string if size of string is less then indicated pad length
224func PadBoth(str string, padStr string, padLen int) string {
225	return buildPadStr(str, padStr, padLen, true, true)
226}
227
228// PadString either left, right or both sides, not the padding string can be unicode and more then one
229// character
230func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
231
232	// When padded length is less then the current string size
233	if padLen < utf8.RuneCountInString(str) {
234		return str
235	}
236
237	padLen -= utf8.RuneCountInString(str)
238
239	targetLen := padLen
240
241	targetLenLeft := targetLen
242	targetLenRight := targetLen
243	if padLeft && padRight {
244		targetLenLeft = padLen / 2
245		targetLenRight = padLen - targetLenLeft
246	}
247
248	strToRepeatLen := utf8.RuneCountInString(padStr)
249
250	repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
251	repeatedString := strings.Repeat(padStr, repeatTimes)
252
253	leftSide := ""
254	if padLeft {
255		leftSide = repeatedString[0:targetLenLeft]
256	}
257
258	rightSide := ""
259	if padRight {
260		rightSide = repeatedString[0:targetLenRight]
261	}
262
263	return leftSide + str + rightSide
264}
265
266// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
267func TruncatingErrorf(str string, args ...interface{}) error {
268	n := strings.Count(str, "%s")
269	return fmt.Errorf(str, args[:n]...)
270}
271