mirror of
https://github.com/strongdm/comply
synced 2024-09-20 19:15:06 +00:00
356 lines
6.9 KiB
Go
356 lines
6.9 KiB
Go
package ace
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// Tag names
|
|
const (
|
|
tagNameDiv = "div"
|
|
)
|
|
|
|
// Attribute names
|
|
const (
|
|
attributeNameID = "id"
|
|
)
|
|
|
|
var inlineElements = map[string]bool{
|
|
"b": true,
|
|
"big": true,
|
|
"i": true,
|
|
"small": true,
|
|
"tt": true,
|
|
"abbr": true,
|
|
"acronym": true,
|
|
"cite": true,
|
|
"code": true,
|
|
"dfn": true,
|
|
"em": true,
|
|
"kbd": true,
|
|
"strong": true,
|
|
"samp": true,
|
|
"time": true,
|
|
"var": true,
|
|
"a": true,
|
|
"bdo": true,
|
|
"br": true,
|
|
"img": true,
|
|
"map": true,
|
|
"object": true,
|
|
"q": true,
|
|
"script": true,
|
|
"span": true,
|
|
"sub": true,
|
|
"sup": true,
|
|
"button": true,
|
|
"input": true,
|
|
"label": true,
|
|
"select": true,
|
|
"textarea": true,
|
|
}
|
|
|
|
// htmlAttribute represents an HTML attribute.
|
|
type htmlAttribute struct {
|
|
key string
|
|
value string
|
|
}
|
|
|
|
// htmlTag represents an HTML tag.
|
|
type htmlTag struct {
|
|
elementBase
|
|
tagName string
|
|
id string
|
|
classes []string
|
|
containPlainText bool
|
|
insertBr bool
|
|
attributes []htmlAttribute
|
|
textValue string
|
|
}
|
|
|
|
// WriteTo writes data to w.
|
|
func (e *htmlTag) WriteTo(w io.Writer) (int64, error) {
|
|
var bf bytes.Buffer
|
|
|
|
// Write an open tag.
|
|
bf.WriteString(lt)
|
|
bf.WriteString(e.tagName)
|
|
// Write an id.
|
|
if e.id != "" {
|
|
bf.WriteString(space)
|
|
bf.WriteString(attributeNameID)
|
|
bf.WriteString(equal)
|
|
bf.WriteString(doubleQuote)
|
|
bf.WriteString(e.id)
|
|
bf.WriteString(doubleQuote)
|
|
}
|
|
// Write classes.
|
|
if len(e.classes) > 0 {
|
|
bf.WriteString(space)
|
|
bf.WriteString(e.opts.AttributeNameClass)
|
|
bf.WriteString(equal)
|
|
bf.WriteString(doubleQuote)
|
|
for i, class := range e.classes {
|
|
if i > 0 {
|
|
bf.WriteString(space)
|
|
}
|
|
bf.WriteString(class)
|
|
}
|
|
bf.WriteString(doubleQuote)
|
|
}
|
|
// Write attributes.
|
|
if len(e.attributes) > 0 {
|
|
|
|
for _, a := range e.attributes {
|
|
bf.WriteString(space)
|
|
bf.WriteString(a.key)
|
|
if a.value != "" {
|
|
bf.WriteString(equal)
|
|
bf.WriteString(doubleQuote)
|
|
bf.WriteString(a.value)
|
|
bf.WriteString(doubleQuote)
|
|
}
|
|
}
|
|
}
|
|
bf.WriteString(gt)
|
|
|
|
// Write a text value
|
|
if e.textValue != "" {
|
|
if e.opts.formatter != nil {
|
|
e.opts.formatter.WritingTextValue(&bf, e)
|
|
}
|
|
bf.WriteString(e.textValue)
|
|
}
|
|
|
|
if e.containPlainText {
|
|
bf.WriteString(lf)
|
|
}
|
|
|
|
// Write children's HTML.
|
|
if i, err := e.writeChildren(&bf); err != nil {
|
|
return i, err
|
|
}
|
|
|
|
// Write a close tag.
|
|
if !e.noCloseTag() {
|
|
if e.opts.formatter != nil {
|
|
e.opts.formatter.ClosingElement(&bf, e)
|
|
}
|
|
bf.WriteString(lt)
|
|
bf.WriteString(slash)
|
|
bf.WriteString(e.tagName)
|
|
bf.WriteString(gt)
|
|
}
|
|
|
|
// Write the buffer.
|
|
i, err := w.Write(bf.Bytes())
|
|
|
|
return int64(i), err
|
|
}
|
|
|
|
// ContainPlainText returns the HTML tag's containPlainText field.
|
|
func (e *htmlTag) ContainPlainText() bool {
|
|
return e.containPlainText
|
|
}
|
|
|
|
// InsertBr returns true if the br tag is inserted to the line.
|
|
func (e *htmlTag) InsertBr() bool {
|
|
return e.insertBr
|
|
}
|
|
|
|
// setAttributes parses the tokens and set attributes to the element.
|
|
func (e *htmlTag) setAttributes() error {
|
|
parsedTokens := e.parseTokens()
|
|
|
|
var i int
|
|
var token string
|
|
var setTextValue bool
|
|
|
|
// Set attributes to the element.
|
|
for i, token = range parsedTokens {
|
|
kv := strings.Split(token, equal)
|
|
|
|
if len(kv) < 2 {
|
|
setTextValue = true
|
|
break
|
|
}
|
|
|
|
k := kv[0]
|
|
v := strings.Join(kv[1:], equal)
|
|
|
|
// Remove the prefix and suffix of the double quotes.
|
|
if len(v) > 1 && strings.HasPrefix(v, doubleQuote) && strings.HasSuffix(v, doubleQuote) {
|
|
v = v[1 : len(v)-1]
|
|
}
|
|
|
|
switch k {
|
|
case attributeNameID:
|
|
if e.id != "" {
|
|
return fmt.Errorf("multiple IDs are specified [file: %s][line: %d]", e.ln.fileName(), e.ln.no)
|
|
}
|
|
e.id = v
|
|
case e.opts.AttributeNameClass:
|
|
e.classes = append(e.classes, strings.Split(v, space)...)
|
|
default:
|
|
e.attributes = append(e.attributes, htmlAttribute{k, v})
|
|
}
|
|
}
|
|
|
|
// Set a text value to the element.
|
|
if setTextValue {
|
|
e.textValue = strings.Join(parsedTokens[i:], space)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// noCloseTag returns true is the HTML tag has no close tag.
|
|
func (e *htmlTag) noCloseTag() bool {
|
|
for _, name := range e.opts.NoCloseTagNames {
|
|
if e.tagName == name {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// true if the element is inline one
|
|
func (e *htmlTag) IsBlockElement() bool {
|
|
if inline, found := inlineElements[e.tagName]; found {
|
|
return !inline
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// newHTMLTag creates and returns an HTML tag.
|
|
func newHTMLTag(ln *line, rslt *result, src *source, parent element, opts *Options) (*htmlTag, error) {
|
|
if len(ln.tokens) < 1 {
|
|
return nil, fmt.Errorf("an HTML tag is not specified [file: %s][line: %d]", ln.fileName(), ln.no)
|
|
}
|
|
|
|
s := ln.tokens[0]
|
|
|
|
tagName := extractTagName(s)
|
|
|
|
id, err := extractID(s, ln)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
classes := extractClasses(s)
|
|
|
|
e := &htmlTag{
|
|
elementBase: newElementBase(ln, rslt, src, parent, opts),
|
|
tagName: tagName,
|
|
id: id,
|
|
classes: classes,
|
|
containPlainText: strings.HasSuffix(s, dot),
|
|
insertBr: strings.HasSuffix(s, doubleDot),
|
|
attributes: make([]htmlAttribute, 0, 2),
|
|
}
|
|
|
|
if err := e.setAttributes(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
// extractTag extracts and returns a tag.
|
|
func extractTagName(s string) string {
|
|
tagName := strings.Split(strings.Split(s, sharp)[0], dot)[0]
|
|
|
|
if tagName == "" {
|
|
tagName = tagNameDiv
|
|
}
|
|
|
|
return tagName
|
|
}
|
|
|
|
// extractID extracts and returns an ID.
|
|
func extractID(s string, ln *line) (string, error) {
|
|
tokens := strings.Split(s, sharp)
|
|
|
|
l := len(tokens)
|
|
|
|
if l < 2 {
|
|
return "", nil
|
|
}
|
|
|
|
if l > 2 {
|
|
return "", fmt.Errorf("multiple IDs are specified [file: %s][line: %d]", ln.fileName(), ln.no)
|
|
}
|
|
|
|
return strings.Split(tokens[1], dot)[0], nil
|
|
}
|
|
|
|
// extractClasses extracts and returns classes.
|
|
func extractClasses(s string) []string {
|
|
var classes []string
|
|
|
|
for i, token := range strings.Split(s, dot) {
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
class := strings.Split(token, sharp)[0]
|
|
|
|
if class == "" {
|
|
continue
|
|
}
|
|
|
|
classes = append(classes, class)
|
|
}
|
|
|
|
return classes
|
|
}
|
|
|
|
// parseTokens parses the tokens and return them
|
|
func (e *htmlTag) parseTokens() []string {
|
|
var inQuote bool
|
|
var inDelim bool
|
|
var tokens []string
|
|
var token string
|
|
|
|
str := strings.Join(e.ln.tokens[1:], space)
|
|
for _, chr := range str {
|
|
switch c := string(chr); c {
|
|
case space:
|
|
if inQuote || inDelim {
|
|
token += c
|
|
} else {
|
|
tokens = append(tokens, token)
|
|
token = ""
|
|
}
|
|
case doubleQuote:
|
|
if !inDelim {
|
|
if inQuote {
|
|
inQuote = false
|
|
} else {
|
|
inQuote = true
|
|
}
|
|
}
|
|
token += c
|
|
default:
|
|
token += c
|
|
if inDelim {
|
|
if strings.HasSuffix(token, e.opts.DelimRight) {
|
|
inDelim = false
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(token, e.opts.DelimLeft) {
|
|
inDelim = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(token) > 0 {
|
|
tokens = append(tokens, token)
|
|
}
|
|
return tokens
|
|
}
|