heredoc.go

  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}