github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/present/style.go (about)

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package present
     6  
     7  import (
     8  	"bytes"
     9  	"html"
    10  	"html/template"
    11  	"strings"
    12  	"unicode"
    13  	"unicode/utf8"
    14  )
    15  
    16  /*
    17  	Fonts are demarcated by an initial and final char bracketing a
    18  	space-delimited word, plus possibly some terminal punctuation.
    19  	The chars are
    20  		_ for italic
    21  		* for bold
    22  		` (back quote) for fixed width.
    23  	Inner appearances of the char become spaces. For instance,
    24  		_this_is_italic_!
    25  	becomes
    26  		<i>this is italic</i>!
    27  */
    28  
    29  func init() {
    30  	funcs["style"] = Style
    31  }
    32  
    33  // Style returns s with HTML entities escaped and font indicators turned into
    34  // HTML font tags.
    35  func Style(s string) template.HTML {
    36  	return template.HTML(font(html.EscapeString(s)))
    37  }
    38  
    39  // font returns s with font indicators turned into HTML font tags.
    40  func font(s string) string {
    41  	if !strings.ContainsAny(s, "[`_*") {
    42  		return s
    43  	}
    44  	words := split(s)
    45  	var b bytes.Buffer
    46  Word:
    47  	for w, word := range words {
    48  		if len(word) < 2 {
    49  			continue Word
    50  		}
    51  		if link, _ := parseInlineLink(word); link != "" {
    52  			words[w] = link
    53  			continue Word
    54  		}
    55  		const marker = "_*`"
    56  		// Initial punctuation is OK but must be peeled off.
    57  		first := strings.IndexAny(word, marker)
    58  		if first == -1 {
    59  			continue Word
    60  		}
    61  		// Opening marker must be at the beginning of the token or else preceded by punctuation.
    62  		if first != 0 {
    63  			r, _ := utf8.DecodeLastRuneInString(word[:first])
    64  			if !unicode.IsPunct(r) {
    65  				continue Word
    66  			}
    67  		}
    68  		open, word := word[:first], word[first:]
    69  		char := word[0] // ASCII is OK.
    70  		close := ""
    71  		switch char {
    72  		default:
    73  			continue Word
    74  		case '_':
    75  			open += "<i>"
    76  			close = "</i>"
    77  		case '*':
    78  			open += "<b>"
    79  			close = "</b>"
    80  		case '`':
    81  			open += "<code>"
    82  			close = "</code>"
    83  		}
    84  		// Closing marker must be at the end of the token or else followed by punctuation.
    85  		last := strings.LastIndex(word, word[:1])
    86  		if last == 0 {
    87  			continue Word
    88  		}
    89  		if last+1 != len(word) {
    90  			r, _ := utf8.DecodeRuneInString(word[last+1:])
    91  			if !unicode.IsPunct(r) {
    92  				continue Word
    93  			}
    94  		}
    95  		head, tail := word[:last+1], word[last+1:]
    96  		b.Reset()
    97  		b.WriteString(open)
    98  		var wid int
    99  		for i := 1; i < len(head)-1; i += wid {
   100  			var r rune
   101  			r, wid = utf8.DecodeRuneInString(head[i:])
   102  			if r != rune(char) {
   103  				// Ordinary character.
   104  				b.WriteRune(r)
   105  				continue
   106  			}
   107  			if head[i+1] != char {
   108  				// Inner char becomes space.
   109  				b.WriteRune(' ')
   110  				continue
   111  			}
   112  			// Doubled char becomes real char.
   113  			// Not worth worrying about "_x__".
   114  			b.WriteByte(char)
   115  			wid++ // Consumed two chars, both ASCII.
   116  		}
   117  		b.WriteString(close) // Write closing tag.
   118  		b.WriteString(tail)  // Restore trailing punctuation.
   119  		words[w] = b.String()
   120  	}
   121  	return strings.Join(words, "")
   122  }
   123  
   124  // split is like strings.Fields but also returns the runs of spaces
   125  // and treats inline links as distinct words.
   126  func split(s string) []string {
   127  	var (
   128  		words = make([]string, 0, 10)
   129  		start = 0
   130  	)
   131  
   132  	// appendWord appends the string s[start:end] to the words slice.
   133  	// If the word contains the beginning of a link, the non-link portion
   134  	// of the word and the entire link are appended as separate words,
   135  	// and the start index is advanced to the end of the link.
   136  	appendWord := func(end int) {
   137  		if j := strings.Index(s[start:end], "[["); j > -1 {
   138  			if _, l := parseInlineLink(s[start+j:]); l > 0 {
   139  				// Append portion before link, if any.
   140  				if j > 0 {
   141  					words = append(words, s[start:start+j])
   142  				}
   143  				// Append link itself.
   144  				words = append(words, s[start+j:start+j+l])
   145  				// Advance start index to end of link.
   146  				start = start + j + l
   147  				return
   148  			}
   149  		}
   150  		// No link; just add the word.
   151  		words = append(words, s[start:end])
   152  		start = end
   153  	}
   154  
   155  	wasSpace := false
   156  	for i, r := range s {
   157  		isSpace := unicode.IsSpace(r)
   158  		if i > start && isSpace != wasSpace {
   159  			appendWord(i)
   160  		}
   161  		wasSpace = isSpace
   162  	}
   163  	for start < len(s) {
   164  		appendWord(len(s))
   165  	}
   166  	return words
   167  }