1// Copyright (c) 2014-2019 TSUYUSATO Kitsune
2// This software is released under the MIT License.
3// http://opensource.org/licenses/mit-license.php
4
5// Package heredoc provides creation of here-documents from raw strings.
6//
7// Golang supports raw-string syntax.
8//
9// doc := `
10// Foo
11// Bar
12// `
13//
14// But raw-string cannot recognize indentation. Thus such content is an indented string, equivalent to
15//
16// "\n\tFoo\n\tBar\n"
17//
18// I dont't want this!
19//
20// However this problem is solved by package heredoc.
21//
22// doc := heredoc.Doc(`
23// Foo
24// Bar
25// `)
26//
27// Is equivalent to
28//
29// "Foo\nBar\n"
30package heredoc
31
32import (
33 "fmt"
34 "strings"
35 "unicode"
36)
37
38const maxInt = int(^uint(0) >> 1)
39
40// Doc returns un-indented string as here-document.
41func Doc(raw string) string {
42 skipFirstLine := false
43 if len(raw) > 0 && raw[0] == '\n' {
44 raw = raw[1:]
45 } else {
46 skipFirstLine = true
47 }
48
49 lines := strings.Split(raw, "\n")
50
51 minIndentSize := getMinIndent(lines, skipFirstLine)
52 lines = removeIndentation(lines, minIndentSize, skipFirstLine)
53
54 return strings.Join(lines, "\n")
55}
56
57// getMinIndent calculates the minimum indentation in lines, excluding empty lines.
58func getMinIndent(lines []string, skipFirstLine bool) int {
59 minIndentSize := maxInt
60
61 for i, line := range lines {
62 if i == 0 && skipFirstLine {
63 continue
64 }
65
66 indentSize := 0
67 for _, r := range []rune(line) {
68 if unicode.IsSpace(r) {
69 indentSize += 1
70 } else {
71 break
72 }
73 }
74
75 if len(line) == indentSize {
76 if i == len(lines)-1 && indentSize < minIndentSize {
77 lines[i] = ""
78 }
79 } else if indentSize < minIndentSize {
80 minIndentSize = indentSize
81 }
82 }
83 return minIndentSize
84}
85
86// removeIndentation removes n characters from the front of each line in lines.
87// Skips first line if skipFirstLine is true, skips empty lines.
88func removeIndentation(lines []string, n int, skipFirstLine bool) []string {
89 for i, line := range lines {
90 if i == 0 && skipFirstLine {
91 continue
92 }
93
94 if len(lines[i]) >= n {
95 lines[i] = line[n:]
96 }
97 }
98 return lines
99}
100
101// Docf returns unindented and formatted string as here-document.
102// Formatting is done as for fmt.Printf().
103func Docf(raw string, args ...interface{}) string {
104 return fmt.Sprintf(Doc(raw), args...)
105}