Source file src/cmd/vendor/rsc.io/markdown/heading.go

     1  // Copyright 2021 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 markdown
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"strings"
    11  )
    12  
    13  type Heading struct {
    14  	Position
    15  	Level int
    16  	Text  *Text
    17  	// The HTML id attribute. The parser populates this field if
    18  	// [Parser.HeadingIDs] is true and the heading ends with text like "{#id}".
    19  	ID string
    20  }
    21  
    22  func (b *Heading) PrintHTML(buf *bytes.Buffer) {
    23  	fmt.Fprintf(buf, "<h%d", b.Level)
    24  	if b.ID != "" {
    25  		fmt.Fprintf(buf, ` id="%s"`, htmlQuoteEscaper.Replace(b.ID))
    26  	}
    27  	buf.WriteByte('>')
    28  	b.Text.PrintHTML(buf)
    29  	fmt.Fprintf(buf, "</h%d>\n", b.Level)
    30  }
    31  
    32  func (b *Heading) printMarkdown(buf *bytes.Buffer, s mdState) {
    33  	// TODO: handle setext headings properly.
    34  	buf.WriteString(s.prefix)
    35  	for i := 0; i < b.Level; i++ {
    36  		buf.WriteByte('#')
    37  	}
    38  	buf.WriteByte(' ')
    39  	// The prefix has already been printed for this line of text.
    40  	s.prefix = ""
    41  	b.Text.printMarkdown(buf, s)
    42  	if b.ID != "" {
    43  		// A heading text is a block, so it ends in a newline. Move the newline
    44  		// after the ID.
    45  		buf.Truncate(buf.Len() - 1)
    46  		fmt.Fprintf(buf, " {#%s}\n", b.ID)
    47  	}
    48  }
    49  
    50  func newATXHeading(p *parseState, s line) (line, bool) {
    51  	peek := s
    52  	var n int
    53  	if peek.trimHeading(&n) {
    54  		s := peek.string()
    55  		s = trimRightSpaceTab(s)
    56  		// Remove trailing '#'s.
    57  		if t := strings.TrimRight(s, "#"); t != trimRightSpaceTab(t) || t == "" {
    58  			s = t
    59  		}
    60  		var id string
    61  		if p.HeadingIDs {
    62  			// Parse and remove ID attribute.
    63  			// It must come before trailing '#'s to more closely follow the spec:
    64  			//    The optional closing sequence of #s must be preceded by spaces or tabs
    65  			//    and may be followed by spaces or tabs only.
    66  			// But Goldmark allows it to come after.
    67  			id, s = extractID(p, s)
    68  
    69  			// Goldmark is strict about the id syntax.
    70  			for _, c := range id {
    71  				if c >= 0x80 || !isLetterDigit(byte(c)) {
    72  					p.corner = true
    73  				}
    74  			}
    75  		}
    76  		pos := Position{p.lineno, p.lineno}
    77  		p.doneBlock(&Heading{pos, n, p.newText(pos, s), id})
    78  		return line{}, true
    79  	}
    80  	return s, false
    81  }
    82  
    83  // extractID removes an ID attribute from s if one is present.
    84  // It returns the attribute value and the resulting string.
    85  // The attribute has the form "{#...}", where the "..." can contain
    86  // any character other than '}'.
    87  // The attribute must be followed only by whitespace.
    88  func extractID(p *parseState, s string) (id, s2 string) {
    89  	i := strings.LastIndexByte(s, '{')
    90  	if i < 0 {
    91  		return "", s
    92  	}
    93  	if i+1 >= len(s) || s[i+1] != '#' {
    94  		p.corner = true // goldmark accepts {}
    95  		return "", s
    96  	}
    97  	j := i + strings.IndexByte(s[i:], '}')
    98  	if j < 0 || trimRightSpaceTab(s[j+1:]) != "" {
    99  		return "", s
   100  	}
   101  	id = strings.TrimSpace(s[i+2 : j])
   102  	if id == "" {
   103  		p.corner = true // goldmark accepts {#}
   104  		return "", s
   105  	}
   106  	return s[i+2 : j], s[:i]
   107  }
   108  
   109  func newSetextHeading(p *parseState, s line) (line, bool) {
   110  	var n int
   111  	peek := s
   112  	if p.nextB() == p.para() && peek.trimSetext(&n) {
   113  		p.closeBlock()
   114  		para, ok := p.last().(*Paragraph)
   115  		if !ok {
   116  			return s, false
   117  		}
   118  		p.deleteLast()
   119  		p.doneBlock(&Heading{Position{para.StartLine, p.lineno}, n, para.Text, ""})
   120  		return line{}, true
   121  	}
   122  	return s, false
   123  }
   124  
   125  func (s *line) trimHeading(width *int) bool {
   126  	t := *s
   127  	t.trimSpace(0, 3, false)
   128  	if !t.trim('#') {
   129  		return false
   130  	}
   131  	n := 1
   132  	for n < 6 && t.trim('#') {
   133  		n++
   134  	}
   135  	if !t.trimSpace(1, 1, true) {
   136  		return false
   137  	}
   138  	*width = n
   139  	*s = t
   140  	return true
   141  }
   142  
   143  func (s *line) trimSetext(n *int) bool {
   144  	t := *s
   145  	t.trimSpace(0, 3, false)
   146  	c := t.peek()
   147  	if c == '-' || c == '=' {
   148  		for t.trim(c) {
   149  		}
   150  		t.skipSpace()
   151  		if t.eof() {
   152  			if c == '=' {
   153  				*n = 1
   154  			} else {
   155  				*n = 2
   156  			}
   157  			return true
   158  		}
   159  	}
   160  	return false
   161  }
   162  

View as plain text