mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-15 05:33:47 +08:00
发布v2.15.1版本,更新内容请查看:https://github.com/bufanyun/hotgo/blob/v2.0/docs/guide-zh-CN/start-update-log.md
This commit is contained in:
7
server/internal/library/hggen/views/gohtml/consts.go
Normal file
7
server/internal/library/hggen/views/gohtml/consts.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package gohtml
|
||||
|
||||
const (
|
||||
defaultIndentString = " "
|
||||
startIndent = 0
|
||||
defaultLastElement = "</html>"
|
||||
)
|
||||
2
server/internal/library/hggen/views/gohtml/doc.go
Normal file
2
server/internal/library/hggen/views/gohtml/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package gohtml provides an HTML formatting function.
|
||||
package gohtml
|
||||
7
server/internal/library/hggen/views/gohtml/element.go
Normal file
7
server/internal/library/hggen/views/gohtml/element.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package gohtml
|
||||
|
||||
// An element represents an HTML element.
|
||||
type element interface {
|
||||
isInline() bool
|
||||
write(*formattedBuffer, bool) bool
|
||||
}
|
||||
37
server/internal/library/hggen/views/gohtml/formatter.go
Normal file
37
server/internal/library/hggen/views/gohtml/formatter.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format parses the input HTML string, formats it and returns the result.
|
||||
func Format(s string) string {
|
||||
return parse(strings.NewReader(s)).html()
|
||||
}
|
||||
|
||||
// FormatBytes parses input HTML as bytes, formats it and returns the result.
|
||||
func FormatBytes(b []byte) []byte {
|
||||
return parse(bytes.NewReader(b)).bytes()
|
||||
}
|
||||
|
||||
// Format parses the input HTML string, formats it and returns the result with line no.
|
||||
func FormatWithLineNo(s string) string {
|
||||
return AddLineNo(Format(s))
|
||||
}
|
||||
|
||||
func AddLineNo(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
maxLineNoStrLen := len(strconv.Itoa(len(lines)))
|
||||
bf := &bytes.Buffer{}
|
||||
for i, line := range lines {
|
||||
lineNoStr := strconv.Itoa(i + 1)
|
||||
if i > 0 {
|
||||
bf.WriteString("\n")
|
||||
}
|
||||
bf.WriteString(strings.Repeat(" ", maxLineNoStrLen-len(lineNoStr)) + lineNoStr + " " + line)
|
||||
}
|
||||
return bf.String()
|
||||
|
||||
}
|
||||
54
server/internal/library/hggen/views/gohtml/html_document.go
Normal file
54
server/internal/library/hggen/views/gohtml/html_document.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Column to wrap lines to (disabled by default)
|
||||
var LineWrapColumn = 0
|
||||
|
||||
// Maxmimum characters a long word can extend past LineWrapColumn without wrapping
|
||||
var LineWrapMaxSpillover = 5
|
||||
|
||||
// An htmlDocument represents an HTML document.
|
||||
type htmlDocument struct {
|
||||
elements []element
|
||||
}
|
||||
|
||||
// html generates an HTML source code and returns it.
|
||||
func (htmlDoc *htmlDocument) html() string {
|
||||
str := string(htmlDoc.bytes())
|
||||
str = replaceMultipleNewlinesWithSpaceAndTabs(str)
|
||||
return str
|
||||
}
|
||||
|
||||
func replaceMultipleNewlinesWithSpaceAndTabs(input string) string {
|
||||
re := regexp.MustCompile(`\n\s*\n+`)
|
||||
formattedString := re.ReplaceAllString(input, "\n")
|
||||
return formattedString
|
||||
}
|
||||
|
||||
// bytes reads from htmlDocument's internal array of elements and returns HTML source code
|
||||
func (htmlDoc *htmlDocument) bytes() []byte {
|
||||
bf := &formattedBuffer{
|
||||
buffer: &bytes.Buffer{},
|
||||
|
||||
lineWrapColumn: LineWrapColumn,
|
||||
lineWrapMaxSpillover: LineWrapMaxSpillover,
|
||||
|
||||
indentString: defaultIndentString,
|
||||
indentLevel: startIndent,
|
||||
}
|
||||
|
||||
isPreviousNodeInline := true
|
||||
for _, child := range htmlDoc.elements {
|
||||
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
|
||||
}
|
||||
return bf.buffer.Bytes()
|
||||
}
|
||||
|
||||
// append appends an element to the htmlDocument.
|
||||
func (htmlDoc *htmlDocument) append(e element) {
|
||||
htmlDoc.elements = append(htmlDoc.elements, e)
|
||||
}
|
||||
127
server/internal/library/hggen/views/gohtml/parser.go
Normal file
127
server/internal/library/hggen/views/gohtml/parser.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"golang.org/x/net/html"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parse parses a stirng and converts it into an html.
|
||||
func parse(r io.Reader) *htmlDocument {
|
||||
htmlDoc := &htmlDocument{}
|
||||
tokenizer := html.NewTokenizer(r)
|
||||
for {
|
||||
if errorToken, _, _ := parseToken(tokenizer, htmlDoc, nil); errorToken {
|
||||
break
|
||||
}
|
||||
}
|
||||
return htmlDoc
|
||||
}
|
||||
|
||||
// Function that identifies which tags will be treated as containing preformatted
|
||||
// content. Such tags will have the formatting of all its contents preserved
|
||||
// unchanged.
|
||||
// The opening tag html.Token is passed to this function.
|
||||
// By default, only <pre> and <textarea> tags are considered preformatted.
|
||||
var IsPreformatted = func(token html.Token) bool {
|
||||
return token.Data == "pre" || token.Data == "textarea"
|
||||
}
|
||||
|
||||
func parseToken(tokenizer *html.Tokenizer, htmlDoc *htmlDocument, parent *tagElement) (bool, bool, string) {
|
||||
tokenType := tokenizer.Next()
|
||||
raw := string(tokenizer.Raw())
|
||||
switch tokenType {
|
||||
case html.ErrorToken:
|
||||
return true, false, ""
|
||||
case html.TextToken:
|
||||
text := string(tokenizer.Raw())
|
||||
if strings.TrimSpace(text) == "" && (parent == nil || !parent.isRaw) {
|
||||
break
|
||||
}
|
||||
textElement := &textElement{text: text, parent: parent}
|
||||
appendElement(htmlDoc, parent, textElement)
|
||||
case html.StartTagToken:
|
||||
raw := string(tokenizer.Raw())
|
||||
token := tokenizer.Token()
|
||||
tagElement := &tagElement{
|
||||
tagName: string(token.Data),
|
||||
startTagRaw: raw,
|
||||
isRaw: IsPreformatted(token) || (parent != nil && parent.isRaw),
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
for {
|
||||
errorToken, parentEnded, unsetEndTag := parseToken(tokenizer, htmlDoc, tagElement)
|
||||
if errorToken {
|
||||
return true, false, ""
|
||||
}
|
||||
if parentEnded {
|
||||
if unsetEndTag != "" {
|
||||
return false, false, unsetEndTag
|
||||
}
|
||||
break
|
||||
}
|
||||
if unsetEndTag != "" {
|
||||
tagName := setEndTagRaw(tokenizer, tagElement, unsetEndTag)
|
||||
return false, false, tagName
|
||||
}
|
||||
}
|
||||
case html.EndTagToken:
|
||||
tagName := setEndTagRaw(tokenizer, parent, getTagName(tokenizer))
|
||||
return false, true, tagName
|
||||
case html.DoctypeToken, html.CommentToken:
|
||||
tagElement := &tagElement{
|
||||
tagName: getTagName(tokenizer),
|
||||
startTagRaw: string(tokenizer.Raw()),
|
||||
isRaw: parent != nil && parent.isRaw,
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
case html.SelfClosingTagToken:
|
||||
tagElement := &tagElement{
|
||||
tagName: getTagName(tokenizer),
|
||||
startTagRaw: raw,
|
||||
isRaw: parent != nil && parent.isRaw,
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
}
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
// appendElement appends the element to the htmlDocument or parent tagElement.
|
||||
func appendElement(htmlDoc *htmlDocument, parent *tagElement, e element) {
|
||||
if parent != nil {
|
||||
parent.appendChild(e)
|
||||
} else {
|
||||
htmlDoc.append(e)
|
||||
}
|
||||
}
|
||||
|
||||
// getTagName gets a tagName from tokenizer.
|
||||
func getTagName(tokenizer *html.Tokenizer) string {
|
||||
tagName, _ := tokenizer.TagName()
|
||||
return string(tagName)
|
||||
}
|
||||
|
||||
// setEndTagRaw sets an endTagRaw to the parent.
|
||||
func setEndTagRaw(tokenizer *html.Tokenizer, parent *tagElement, tagName string) string {
|
||||
if parent != nil && parent.tagName == tagName {
|
||||
parent.endTagRaw = `</` + fMustCompile(parent.startTagRaw) + `>` //string(tokenizer.Raw())
|
||||
return ""
|
||||
}
|
||||
return tagName
|
||||
}
|
||||
|
||||
func fMustCompile(input string) (result string) {
|
||||
re := regexp.MustCompile(`<([A-Za-z-]+)[\s\S]*?>`)
|
||||
match := re.FindStringSubmatch(input)
|
||||
|
||||
if len(match) > 1 {
|
||||
result = match[1]
|
||||
} else {
|
||||
result = input
|
||||
}
|
||||
return
|
||||
}
|
||||
145
server/internal/library/hggen/views/gohtml/tag_element.go
Normal file
145
server/internal/library/hggen/views/gohtml/tag_element.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// A tagElement represents a tag element of an HTML document.
|
||||
type tagElement struct {
|
||||
tagName string
|
||||
startTagRaw string
|
||||
endTagRaw string
|
||||
|
||||
parent *tagElement
|
||||
children []element
|
||||
|
||||
isRaw bool
|
||||
isChildrenInlineCache *bool
|
||||
}
|
||||
|
||||
// Enable condensing a tag with only inline children onto a single line, or
|
||||
// completely inlining it with sibling nodes.
|
||||
// Tags to be treated as inline can be set in `InlineTags`.
|
||||
// Only inline tags will be completely inlined, while other condensable tags
|
||||
// will be given their own dedicated (single) line.
|
||||
var Condense bool
|
||||
|
||||
// Tags that are considered inline tags.
|
||||
// Note: Text nodes are always considered to be inline
|
||||
var InlineTags = map[string]bool{
|
||||
"a": true,
|
||||
"code": true,
|
||||
"em": true,
|
||||
"span": true,
|
||||
"strong": true,
|
||||
}
|
||||
|
||||
// Maximum length of an opening inline tag before it's un-inlined
|
||||
var InlineTagMaxLength = 40
|
||||
|
||||
func (e *tagElement) isInline() bool {
|
||||
if e.isRaw || !InlineTags[e.tagName] || len(e.startTagRaw) > InlineTagMaxLength {
|
||||
return false
|
||||
}
|
||||
return e.isChildrenInline()
|
||||
}
|
||||
|
||||
func (e *tagElement) isChildrenInline() bool {
|
||||
if !Condense {
|
||||
return false
|
||||
}
|
||||
if e.isChildrenInlineCache != nil {
|
||||
return *e.isChildrenInlineCache
|
||||
}
|
||||
|
||||
isInline := true
|
||||
for _, child := range e.children {
|
||||
isInline = isInline && child.isInline()
|
||||
}
|
||||
|
||||
e.isChildrenInlineCache = &isInline
|
||||
return isInline
|
||||
}
|
||||
|
||||
// write writes a tag to the buffer.
|
||||
func (e *tagElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
|
||||
if e.isRaw {
|
||||
if e.parent != nil && !e.parent.isRaw {
|
||||
bf.writeLineFeed()
|
||||
bf.writeIndent()
|
||||
bf.rawMode = true
|
||||
defer func() {
|
||||
bf.rawMode = false
|
||||
}()
|
||||
}
|
||||
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
for _, child := range e.children {
|
||||
child.write(bf, true)
|
||||
}
|
||||
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
return false
|
||||
}
|
||||
|
||||
if e.isChildrenInline() && (e.endTagRaw != "" || e.isInline()) {
|
||||
// Write the condensed output to a separate buffer, in case it doesn't work out
|
||||
condensedBuffer := *bf
|
||||
condensedBuffer.buffer = &bytes.Buffer{}
|
||||
|
||||
if bf.buffer.Len() > 0 && (!isPreviousNodeInline || !e.isInline()) {
|
||||
condensedBuffer.writeLineFeed()
|
||||
}
|
||||
condensedBuffer.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
if !isPreviousNodeInline && e.endTagRaw != "" {
|
||||
condensedBuffer.indentLevel++
|
||||
}
|
||||
|
||||
for _, child := range e.children {
|
||||
child.write(&condensedBuffer, true)
|
||||
}
|
||||
if e.endTagRaw != "" {
|
||||
condensedBuffer.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
if !isPreviousNodeInline {
|
||||
condensedBuffer.indentLevel--
|
||||
}
|
||||
}
|
||||
|
||||
if e.isInline() || bytes.IndexAny(condensedBuffer.buffer.Bytes()[1:], "\n") == -1 {
|
||||
// If we're an inline tag, or there were no newlines were in the buffer,
|
||||
// replace the original with the condensed version
|
||||
condensedBuffer.buffer = bytes.NewBuffer(bytes.Join([][]byte{
|
||||
bf.buffer.Bytes(), condensedBuffer.buffer.Bytes(),
|
||||
}, []byte{}))
|
||||
*bf = condensedBuffer
|
||||
|
||||
return e.isInline()
|
||||
}
|
||||
}
|
||||
|
||||
if bf.buffer.Len() > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
if e.endTagRaw != "" {
|
||||
bf.indentLevel++
|
||||
}
|
||||
|
||||
isPreviousNodeInline = false
|
||||
for _, child := range e.children {
|
||||
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
|
||||
}
|
||||
|
||||
if e.endTagRaw != "" {
|
||||
if len(e.children) > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
bf.indentLevel--
|
||||
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// appendChild append an element to the element's children.
|
||||
func (e *tagElement) appendChild(child element) {
|
||||
e.children = append(e.children, child)
|
||||
}
|
||||
43
server/internal/library/hggen/views/gohtml/text_element.go
Normal file
43
server/internal/library/hggen/views/gohtml/text_element.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A textElement represents a text element of an HTML document.
|
||||
type textElement struct {
|
||||
text string
|
||||
parent *tagElement
|
||||
}
|
||||
|
||||
func (e *textElement) isInline() bool {
|
||||
// Text nodes are always considered to be inline
|
||||
return true
|
||||
}
|
||||
|
||||
// write writes a text to the buffer.
|
||||
func (e *textElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
|
||||
text := unifyLineFeed(e.text)
|
||||
if e.parent != nil && e.parent.isRaw {
|
||||
bf.writeToken(text, formatterTokenType_Text)
|
||||
return true
|
||||
}
|
||||
|
||||
if !isPreviousNodeInline {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
|
||||
// Collapse leading and trailing spaces
|
||||
text = regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(text, " ")
|
||||
lines := strings.Split(text, "\n")
|
||||
for l, line := range lines {
|
||||
if l > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
for _, word := range strings.Split(line, " ") {
|
||||
bf.writeToken(word, formatterTokenType_Text)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
101
server/internal/library/hggen/views/gohtml/utils.go
Normal file
101
server/internal/library/hggen/views/gohtml/utils.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type formatterTokenType int
|
||||
|
||||
const (
|
||||
formatterTokenType_Nothing formatterTokenType = iota
|
||||
formatterTokenType_Tag
|
||||
formatterTokenType_Text
|
||||
)
|
||||
|
||||
type formattedBuffer struct {
|
||||
buffer *bytes.Buffer
|
||||
rawMode bool
|
||||
|
||||
indentString string
|
||||
indentLevel int
|
||||
|
||||
lineWrapColumn int
|
||||
lineWrapMaxSpillover int
|
||||
|
||||
curLineLength int
|
||||
prevTokenType formatterTokenType
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeLineFeed() {
|
||||
if !bf.rawMode {
|
||||
// Strip trailing newlines
|
||||
bf.buffer = bytes.NewBuffer(bytes.TrimRightFunc(
|
||||
bf.buffer.Bytes(),
|
||||
func(r rune) bool {
|
||||
return r != '\n' && unicode.IsSpace(r)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
bf.buffer.WriteString("\n")
|
||||
bf.curLineLength = 0
|
||||
bf.prevTokenType = formatterTokenType_Nothing
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeIndent() {
|
||||
bf.buffer.WriteString(strings.Repeat(bf.indentString, bf.indentLevel))
|
||||
bf.curLineLength += len(bf.indentString) * bf.indentLevel
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeToken(token string, kind formatterTokenType) {
|
||||
if bf.rawMode {
|
||||
bf.buffer.WriteString(token)
|
||||
bf.curLineLength += len(token)
|
||||
return
|
||||
}
|
||||
|
||||
if bf.prevTokenType == formatterTokenType_Nothing && strings.TrimSpace(token) == "" {
|
||||
// It's a whitespace token, but we already have indentation which functions
|
||||
// the same, so we ignore it
|
||||
return
|
||||
}
|
||||
|
||||
toWrite := token
|
||||
if kind == formatterTokenType_Text && bf.prevTokenType == formatterTokenType_Text {
|
||||
toWrite = " " + token
|
||||
}
|
||||
|
||||
if bf.prevTokenType != formatterTokenType_Nothing && bf.lineWrapColumn > 0 {
|
||||
switch {
|
||||
case bf.curLineLength > bf.lineWrapColumn:
|
||||
// Current line is too long
|
||||
fallthrough
|
||||
|
||||
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn+bf.lineWrapMaxSpillover:
|
||||
// Current line + new token is too long even with allowed spillover
|
||||
fallthrough
|
||||
|
||||
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn &&
|
||||
bf.curLineLength > bf.lineWrapColumn-bf.lineWrapMaxSpillover:
|
||||
// Current line + new token is too long and doesn't quality for spillover
|
||||
|
||||
bf.writeLineFeed()
|
||||
bf.writeToken(token, kind)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if bf.curLineLength == 0 {
|
||||
bf.writeIndent()
|
||||
}
|
||||
bf.buffer.WriteString(toWrite)
|
||||
bf.curLineLength += len(toWrite)
|
||||
bf.prevTokenType = kind
|
||||
}
|
||||
|
||||
// unifyLineFeed unifies line feeds.
|
||||
func unifyLineFeed(s string) string {
|
||||
return strings.Replace(strings.Replace(s, "\r\n", "\n", -1), "\r", "\n", -1)
|
||||
}
|
||||
33
server/internal/library/hggen/views/gohtml/writer.go
Normal file
33
server/internal/library/hggen/views/gohtml/writer.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// A Writer represents a formatted HTML source codes writer.
|
||||
type Writer struct {
|
||||
writer io.Writer
|
||||
lastElement string
|
||||
bf *bytes.Buffer
|
||||
}
|
||||
|
||||
// SetLastElement set the lastElement to the Writer.
|
||||
func (wr *Writer) SetLastElement(lastElement string) *Writer {
|
||||
wr.lastElement = lastElement
|
||||
return wr
|
||||
}
|
||||
|
||||
// Write writes the parameter.
|
||||
func (wr *Writer) Write(p []byte) (n int, err error) {
|
||||
n, _ = wr.bf.Write(p) // (*bytes.Buffer).Write never produces an error
|
||||
if bytes.HasSuffix(p, []byte(wr.lastElement)) {
|
||||
_, err = wr.writer.Write([]byte(Format(wr.bf.String()) + "\n"))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// NewWriter generates a Writer and returns it.
|
||||
func NewWriter(wr io.Writer) *Writer {
|
||||
return &Writer{writer: wr, lastElement: defaultLastElement, bf: &bytes.Buffer{}}
|
||||
}
|
||||
Reference in New Issue
Block a user