- Static compiler with STRC pattern (Static Template Resolution with
Compartmentalized Layers)
- Template syntax: { } interpolation, { s-for }, { s-if/s-elif/s-else
}
- File types: .strata, .compiler.sts, .service.sts, .api.sts, .sts,
.scss
- CLI tools: strata-compile dev, strata-compile build, strata-compile
g (generators)
- create-strata-compile scaffolding CLI with Pokemon API example
- Dev server with WebSocket HMR (Hot Module Replacement)
- Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
LICENSE
689 lines
17 KiB
Go
689 lines
17 KiB
Go
package parser
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/CarGDev/strata-compile/internal/ast"
|
|
)
|
|
|
|
// StrataParser parses .strata files
|
|
type StrataParser struct {
|
|
source string
|
|
pos int
|
|
template *ast.TemplateNode
|
|
script *ast.ScriptNode
|
|
style *ast.StyleNode
|
|
}
|
|
|
|
// NewStrataParser creates a new parser
|
|
func NewStrataParser(source string) *StrataParser {
|
|
return &StrataParser{
|
|
source: source,
|
|
pos: 0,
|
|
}
|
|
}
|
|
|
|
// Parse parses a .strata file into AST nodes
|
|
func (p *StrataParser) Parse() (*ast.StrataFile, error) {
|
|
file := &ast.StrataFile{}
|
|
|
|
// Extract <template>, <script>, and <style> blocks
|
|
templateContent := p.extractBlock("template")
|
|
scriptContent := p.extractBlock("script")
|
|
styleContent := p.extractBlock("style")
|
|
|
|
// Parse template
|
|
if templateContent != "" {
|
|
template, err := p.parseTemplate(templateContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file.Template = template
|
|
}
|
|
|
|
// Parse script
|
|
if scriptContent != "" {
|
|
script, err := p.parseScript(scriptContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file.Script = script
|
|
}
|
|
|
|
// Parse style
|
|
if styleContent != "" {
|
|
style, err := p.parseStyle(styleContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file.Style = style
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
// extractBlock extracts content between <tag> and </tag>
|
|
func (p *StrataParser) extractBlock(tag string) string {
|
|
// Match opening tag with optional attributes
|
|
openPattern := regexp.MustCompile(`<` + tag + `(?:\s+[^>]*)?>`)
|
|
closePattern := regexp.MustCompile(`</` + tag + `>`)
|
|
|
|
openMatch := openPattern.FindStringIndex(p.source)
|
|
if openMatch == nil {
|
|
return ""
|
|
}
|
|
|
|
closeMatch := closePattern.FindStringIndex(p.source[openMatch[1]:])
|
|
if closeMatch == nil {
|
|
return ""
|
|
}
|
|
|
|
// Extract content between tags
|
|
start := openMatch[1]
|
|
end := openMatch[1] + closeMatch[0]
|
|
|
|
return strings.TrimSpace(p.source[start:end])
|
|
}
|
|
|
|
// parseTemplate parses the template content
|
|
func (p *StrataParser) parseTemplate(content string) (*ast.TemplateNode, error) {
|
|
template := &ast.TemplateNode{
|
|
Children: make([]ast.Node, 0),
|
|
}
|
|
|
|
// Parse HTML with Strata directives
|
|
nodes, err := p.parseHTML(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
template.Children = nodes
|
|
return template, nil
|
|
}
|
|
|
|
// parseHTML parses HTML content into AST nodes
|
|
func (p *StrataParser) parseHTML(content string) ([]ast.Node, error) {
|
|
nodes := make([]ast.Node, 0)
|
|
|
|
// Simple tokenization - in production, use proper HTML parser
|
|
pos := 0
|
|
for pos < len(content) {
|
|
// Skip whitespace
|
|
for pos < len(content) && isWhitespace(content[pos]) {
|
|
pos++
|
|
}
|
|
|
|
if pos >= len(content) {
|
|
break
|
|
}
|
|
|
|
// Strata block directive { s-* } or variable interpolation { var }
|
|
if content[pos] == '{' && pos+1 < len(content) && content[pos+1] != '{' {
|
|
// Find closing }
|
|
end := strings.Index(content[pos:], "}")
|
|
if end != -1 {
|
|
inner := strings.TrimSpace(content[pos+1 : pos+end])
|
|
|
|
// Check for block directives
|
|
if strings.HasPrefix(inner, "s-for ") {
|
|
// Parse { s-for item in items } ... { /s-for }
|
|
node, newPos, err := p.parseForBlock(content, pos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = append(nodes, node)
|
|
pos = newPos
|
|
continue
|
|
} else if strings.HasPrefix(inner, "s-if ") {
|
|
// Parse { s-if cond } ... { /s-if }
|
|
node, newPos, err := p.parseIfBlock(content, pos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = append(nodes, node)
|
|
pos = newPos
|
|
continue
|
|
} else if strings.HasPrefix(inner, "s-imp ") {
|
|
// Parse { s-imp "@alias/file" }
|
|
importPath := strings.TrimSpace(inner[6:])
|
|
importPath = strings.Trim(importPath, `"'`)
|
|
nodes = append(nodes, &ast.ImportNode{
|
|
Path: importPath,
|
|
})
|
|
pos = pos + end + 1
|
|
continue
|
|
} else if strings.HasPrefix(inner, "/s-") || strings.HasPrefix(inner, "s-elif") || inner == "s-else" {
|
|
// Closing or branch tags - handled by parent parser
|
|
break
|
|
} else {
|
|
// Variable interpolation { var }
|
|
nodes = append(nodes, &ast.InterpolationNode{
|
|
Expression: inner,
|
|
})
|
|
pos = pos + end + 1
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Legacy {{ }} interpolation (for backwards compatibility)
|
|
if pos+1 < len(content) && content[pos:pos+2] == "{{" {
|
|
end := strings.Index(content[pos:], "}}")
|
|
if end != -1 {
|
|
expr := strings.TrimSpace(content[pos+2 : pos+end])
|
|
nodes = append(nodes, &ast.InterpolationNode{
|
|
Expression: expr,
|
|
})
|
|
pos = pos + end + 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
// HTML Element
|
|
if content[pos] == '<' {
|
|
// Comment
|
|
if pos+4 < len(content) && content[pos:pos+4] == "<!--" {
|
|
end := strings.Index(content[pos:], "-->")
|
|
if end != -1 {
|
|
pos = pos + end + 3
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Closing tag
|
|
if pos+2 < len(content) && content[pos+1] == '/' {
|
|
end := strings.Index(content[pos:], ">")
|
|
if end != -1 {
|
|
pos = pos + end + 1
|
|
}
|
|
break // Return to parent
|
|
}
|
|
|
|
// Opening tag
|
|
node, newPos, err := p.parseElement(content, pos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if node != nil {
|
|
nodes = append(nodes, node)
|
|
}
|
|
pos = newPos
|
|
continue
|
|
}
|
|
|
|
// Text content
|
|
textEnd := strings.IndexAny(content[pos:], "<{")
|
|
if textEnd == -1 {
|
|
textEnd = len(content) - pos
|
|
}
|
|
text := strings.TrimSpace(content[pos : pos+textEnd])
|
|
if text != "" {
|
|
nodes = append(nodes, &ast.TextNode{Content: text})
|
|
}
|
|
pos = pos + textEnd
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
// parseForBlock parses { s-for item in items } ... { /s-for }
|
|
func (p *StrataParser) parseForBlock(content string, pos int) (*ast.ForBlockNode, int, error) {
|
|
// Find opening tag end
|
|
openEnd := strings.Index(content[pos:], "}")
|
|
if openEnd == -1 {
|
|
return nil, pos, nil
|
|
}
|
|
|
|
// Parse the for expression: "s-for item in items" or "s-for item, i in items"
|
|
inner := strings.TrimSpace(content[pos+1 : pos+openEnd])
|
|
inner = strings.TrimPrefix(inner, "s-for ")
|
|
inner = strings.TrimSpace(inner)
|
|
|
|
// Split by " in "
|
|
parts := strings.SplitN(inner, " in ", 2)
|
|
if len(parts) != 2 {
|
|
return nil, pos + openEnd + 1, nil
|
|
}
|
|
|
|
forNode := &ast.ForBlockNode{
|
|
Iterable: strings.TrimSpace(parts[1]),
|
|
}
|
|
|
|
// Parse item and optional index: "item" or "item, i"
|
|
itemPart := strings.TrimSpace(parts[0])
|
|
if strings.Contains(itemPart, ",") {
|
|
itemParts := strings.SplitN(itemPart, ",", 2)
|
|
forNode.Item = strings.TrimSpace(itemParts[0])
|
|
forNode.Index = strings.TrimSpace(itemParts[1])
|
|
} else {
|
|
forNode.Item = itemPart
|
|
}
|
|
|
|
// Find content between opening and closing tags
|
|
startPos := pos + openEnd + 1
|
|
closeTag := "{ /s-for }"
|
|
closeTagAlt := "{/s-for}"
|
|
|
|
// Search for closing tag
|
|
closePos := strings.Index(content[startPos:], closeTag)
|
|
if closePos == -1 {
|
|
closePos = strings.Index(content[startPos:], closeTagAlt)
|
|
if closePos == -1 {
|
|
// Try more lenient matching
|
|
closePos = p.findClosingTag(content[startPos:], "/s-for")
|
|
}
|
|
}
|
|
|
|
if closePos == -1 {
|
|
return nil, pos + openEnd + 1, nil
|
|
}
|
|
|
|
// Parse children
|
|
childContent := content[startPos : startPos+closePos]
|
|
children, err := p.parseHTML(childContent)
|
|
if err != nil {
|
|
return nil, pos, err
|
|
}
|
|
forNode.Children = children
|
|
|
|
// Calculate new position after closing tag
|
|
newPos := startPos + closePos
|
|
// Skip past the closing tag
|
|
closeEnd := strings.Index(content[newPos:], "}")
|
|
if closeEnd != -1 {
|
|
newPos = newPos + closeEnd + 1
|
|
}
|
|
|
|
return forNode, newPos, nil
|
|
}
|
|
|
|
// parseIfBlock parses { s-if cond } ... { s-elif cond } ... { s-else } ... { /s-if }
|
|
func (p *StrataParser) parseIfBlock(content string, pos int) (*ast.IfBlockNode, int, error) {
|
|
// Find opening tag end
|
|
openEnd := strings.Index(content[pos:], "}")
|
|
if openEnd == -1 {
|
|
return nil, pos, nil
|
|
}
|
|
|
|
// Parse the if condition
|
|
inner := strings.TrimSpace(content[pos+1 : pos+openEnd])
|
|
condition := strings.TrimPrefix(inner, "s-if ")
|
|
condition = strings.TrimSpace(condition)
|
|
|
|
ifNode := &ast.IfBlockNode{
|
|
Condition: condition,
|
|
ElseIf: make([]*ast.ElseIfBranch, 0),
|
|
}
|
|
|
|
// Find content and branches
|
|
startPos := pos + openEnd + 1
|
|
currentPos := startPos
|
|
|
|
// Parse until we hit /s-if, collecting elif and else branches
|
|
for currentPos < len(content) {
|
|
// Look for next branch or closing tag
|
|
nextBrace := strings.Index(content[currentPos:], "{")
|
|
if nextBrace == -1 {
|
|
break
|
|
}
|
|
|
|
bracePos := currentPos + nextBrace
|
|
braceEnd := strings.Index(content[bracePos:], "}")
|
|
if braceEnd == -1 {
|
|
break
|
|
}
|
|
|
|
braceContent := strings.TrimSpace(content[bracePos+1 : bracePos+braceEnd])
|
|
|
|
if strings.HasPrefix(braceContent, "/s-if") {
|
|
// Closing tag - parse content up to here
|
|
childContent := content[startPos:bracePos]
|
|
children, err := p.parseHTML(childContent)
|
|
if err != nil {
|
|
return nil, pos, err
|
|
}
|
|
|
|
// Add children to appropriate branch
|
|
if len(ifNode.ElseIf) == 0 && len(ifNode.Else) == 0 {
|
|
ifNode.Children = children
|
|
}
|
|
|
|
return ifNode, bracePos + braceEnd + 1, nil
|
|
} else if strings.HasPrefix(braceContent, "s-elif ") {
|
|
// Parse content for previous branch
|
|
childContent := content[startPos:bracePos]
|
|
children, err := p.parseHTML(childContent)
|
|
if err != nil {
|
|
return nil, pos, err
|
|
}
|
|
|
|
if len(ifNode.ElseIf) == 0 {
|
|
ifNode.Children = children
|
|
} else {
|
|
ifNode.ElseIf[len(ifNode.ElseIf)-1].Children = children
|
|
}
|
|
|
|
// Add new elif branch
|
|
elifCond := strings.TrimPrefix(braceContent, "s-elif ")
|
|
elifCond = strings.TrimSpace(elifCond)
|
|
ifNode.ElseIf = append(ifNode.ElseIf, &ast.ElseIfBranch{
|
|
Condition: elifCond,
|
|
})
|
|
|
|
startPos = bracePos + braceEnd + 1
|
|
currentPos = startPos
|
|
continue
|
|
} else if braceContent == "s-else" {
|
|
// Parse content for previous branch
|
|
childContent := content[startPos:bracePos]
|
|
children, err := p.parseHTML(childContent)
|
|
if err != nil {
|
|
return nil, pos, err
|
|
}
|
|
|
|
if len(ifNode.ElseIf) == 0 {
|
|
ifNode.Children = children
|
|
} else {
|
|
ifNode.ElseIf[len(ifNode.ElseIf)-1].Children = children
|
|
}
|
|
|
|
// Find closing tag and parse else content
|
|
elseStart := bracePos + braceEnd + 1
|
|
closePos := p.findClosingTag(content[elseStart:], "/s-if")
|
|
if closePos != -1 {
|
|
elseContent := content[elseStart : elseStart+closePos]
|
|
elseChildren, err := p.parseHTML(elseContent)
|
|
if err != nil {
|
|
return nil, pos, err
|
|
}
|
|
ifNode.Else = elseChildren
|
|
|
|
// Skip to after closing tag
|
|
closeEnd := strings.Index(content[elseStart+closePos:], "}")
|
|
if closeEnd != -1 {
|
|
return ifNode, elseStart + closePos + closeEnd + 1, nil
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
currentPos = bracePos + braceEnd + 1
|
|
}
|
|
|
|
return ifNode, currentPos, nil
|
|
}
|
|
|
|
// findClosingTag finds position of { /tag } allowing for whitespace
|
|
func (p *StrataParser) findClosingTag(content string, tag string) int {
|
|
pos := 0
|
|
for pos < len(content) {
|
|
bracePos := strings.Index(content[pos:], "{")
|
|
if bracePos == -1 {
|
|
return -1
|
|
}
|
|
bracePos += pos
|
|
|
|
braceEnd := strings.Index(content[bracePos:], "}")
|
|
if braceEnd == -1 {
|
|
return -1
|
|
}
|
|
|
|
inner := strings.TrimSpace(content[bracePos+1 : bracePos+braceEnd])
|
|
if inner == tag || strings.TrimSpace(inner) == tag {
|
|
return bracePos
|
|
}
|
|
|
|
pos = bracePos + braceEnd + 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// parseElement parses an HTML element
|
|
func (p *StrataParser) parseElement(content string, pos int) (ast.Node, int, error) {
|
|
// Find tag name
|
|
tagStart := pos + 1
|
|
tagEnd := tagStart
|
|
for tagEnd < len(content) && !isWhitespace(content[tagEnd]) && content[tagEnd] != '>' && content[tagEnd] != '/' {
|
|
tagEnd++
|
|
}
|
|
|
|
tagName := content[tagStart:tagEnd]
|
|
|
|
element := &ast.ElementNode{
|
|
Tag: tagName,
|
|
Attributes: make(map[string]string),
|
|
Directives: make([]ast.Directive, 0),
|
|
Children: make([]ast.Node, 0),
|
|
}
|
|
|
|
// Parse attributes
|
|
attrPos := tagEnd
|
|
for attrPos < len(content) && content[attrPos] != '>' && content[attrPos] != '/' {
|
|
// Skip whitespace
|
|
for attrPos < len(content) && isWhitespace(content[attrPos]) {
|
|
attrPos++
|
|
}
|
|
|
|
if attrPos >= len(content) || content[attrPos] == '>' || content[attrPos] == '/' {
|
|
break
|
|
}
|
|
|
|
// Parse attribute name
|
|
nameStart := attrPos
|
|
for attrPos < len(content) && !isWhitespace(content[attrPos]) && content[attrPos] != '=' && content[attrPos] != '>' {
|
|
attrPos++
|
|
}
|
|
attrName := content[nameStart:attrPos]
|
|
|
|
// Parse attribute value
|
|
attrValue := ""
|
|
for attrPos < len(content) && isWhitespace(content[attrPos]) {
|
|
attrPos++
|
|
}
|
|
if attrPos < len(content) && content[attrPos] == '=' {
|
|
attrPos++
|
|
for attrPos < len(content) && isWhitespace(content[attrPos]) {
|
|
attrPos++
|
|
}
|
|
if attrPos < len(content) && (content[attrPos] == '"' || content[attrPos] == '\'') {
|
|
quote := content[attrPos]
|
|
attrPos++
|
|
valueStart := attrPos
|
|
for attrPos < len(content) && content[attrPos] != quote {
|
|
attrPos++
|
|
}
|
|
attrValue = content[valueStart:attrPos]
|
|
attrPos++
|
|
}
|
|
}
|
|
|
|
// Check for directives
|
|
if directive := p.parseDirective(attrName, attrValue); directive != nil {
|
|
element.Directives = append(element.Directives, *directive)
|
|
} else {
|
|
element.Attributes[attrName] = attrValue
|
|
}
|
|
}
|
|
|
|
// Self-closing tag
|
|
if attrPos < len(content) && content[attrPos] == '/' {
|
|
attrPos++
|
|
for attrPos < len(content) && content[attrPos] != '>' {
|
|
attrPos++
|
|
}
|
|
return element, attrPos + 1, nil
|
|
}
|
|
|
|
// Find closing >
|
|
for attrPos < len(content) && content[attrPos] != '>' {
|
|
attrPos++
|
|
}
|
|
attrPos++ // Skip >
|
|
|
|
// Void elements (no children)
|
|
voidElements := map[string]bool{
|
|
"area": true, "base": true, "br": true, "col": true,
|
|
"embed": true, "hr": true, "img": true, "input": true,
|
|
"link": true, "meta": true, "param": true, "source": true,
|
|
"track": true, "wbr": true,
|
|
}
|
|
|
|
if voidElements[tagName] {
|
|
return element, attrPos, nil
|
|
}
|
|
|
|
// Parse children
|
|
childContent := ""
|
|
depth := 1
|
|
childStart := attrPos
|
|
for attrPos < len(content) && depth > 0 {
|
|
if attrPos+1 < len(content) && content[attrPos] == '<' {
|
|
if content[attrPos+1] == '/' {
|
|
// Closing tag
|
|
closeEnd := strings.Index(content[attrPos:], ">")
|
|
if closeEnd != -1 {
|
|
closeTag := content[attrPos+2 : attrPos+closeEnd]
|
|
closeTag = strings.TrimSpace(closeTag)
|
|
if closeTag == tagName {
|
|
depth--
|
|
if depth == 0 {
|
|
childContent = content[childStart:attrPos]
|
|
attrPos = attrPos + closeEnd + 1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else if content[attrPos+1] != '!' {
|
|
// Opening tag - check if same tag
|
|
openEnd := attrPos + 1
|
|
for openEnd < len(content) && !isWhitespace(content[openEnd]) && content[openEnd] != '>' && content[openEnd] != '/' {
|
|
openEnd++
|
|
}
|
|
openTag := content[attrPos+1 : openEnd]
|
|
if openTag == tagName {
|
|
depth++
|
|
}
|
|
}
|
|
}
|
|
attrPos++
|
|
}
|
|
|
|
// Parse children
|
|
if childContent != "" {
|
|
children, err := p.parseHTML(childContent)
|
|
if err != nil {
|
|
return nil, attrPos, err
|
|
}
|
|
element.Children = children
|
|
}
|
|
|
|
return element, attrPos, nil
|
|
}
|
|
|
|
// parseDirective parses Strata directives
|
|
func (p *StrataParser) parseDirective(name, value string) *ast.Directive {
|
|
directive := &ast.Directive{
|
|
Name: name,
|
|
Value: value,
|
|
}
|
|
|
|
// s-if, s-else-if, s-else
|
|
if name == "s-if" || name == "s-else-if" || name == "s-else" {
|
|
directive.Type = ast.DirectiveConditional
|
|
return directive
|
|
}
|
|
|
|
// s-for
|
|
if name == "s-for" {
|
|
directive.Type = ast.DirectiveLoop
|
|
return directive
|
|
}
|
|
|
|
// s-model (two-way binding)
|
|
if name == "s-model" {
|
|
directive.Type = ast.DirectiveModel
|
|
return directive
|
|
}
|
|
|
|
// s-on:event or @event (event handlers)
|
|
if strings.HasPrefix(name, "s-on:") || strings.HasPrefix(name, "@") {
|
|
directive.Type = ast.DirectiveEvent
|
|
if strings.HasPrefix(name, "@") {
|
|
directive.Arg = name[1:]
|
|
} else {
|
|
directive.Arg = name[5:]
|
|
}
|
|
return directive
|
|
}
|
|
|
|
// :attr (attribute binding)
|
|
if strings.HasPrefix(name, ":") {
|
|
directive.Type = ast.DirectiveBind
|
|
directive.Arg = name[1:]
|
|
return directive
|
|
}
|
|
|
|
// s-client:* (hydration strategies)
|
|
if strings.HasPrefix(name, "s-client:") {
|
|
directive.Type = ast.DirectiveClient
|
|
directive.Arg = name[9:]
|
|
return directive
|
|
}
|
|
|
|
// s-fetch (data fetching)
|
|
if name == "s-fetch" {
|
|
directive.Type = ast.DirectiveFetch
|
|
return directive
|
|
}
|
|
|
|
// s-ref
|
|
if name == "s-ref" {
|
|
directive.Type = ast.DirectiveRef
|
|
return directive
|
|
}
|
|
|
|
// s-html (raw HTML)
|
|
if name == "s-html" {
|
|
directive.Type = ast.DirectiveHTML
|
|
return directive
|
|
}
|
|
|
|
// s-static (no JS)
|
|
if name == "s-static" {
|
|
directive.Type = ast.DirectiveStatic
|
|
return directive
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseScript parses the script content
|
|
func (p *StrataParser) parseScript(content string) (*ast.ScriptNode, error) {
|
|
return &ast.ScriptNode{
|
|
Content: content,
|
|
Lang: "ts", // Default to TypeScript
|
|
}, nil
|
|
}
|
|
|
|
// parseStyle parses the style content
|
|
func (p *StrataParser) parseStyle(content string) (*ast.StyleNode, error) {
|
|
// Check for lang attribute in original source
|
|
langPattern := regexp.MustCompile(`<style\s+lang="([^"]+)"`)
|
|
matches := langPattern.FindStringSubmatch(p.source)
|
|
lang := "css"
|
|
if len(matches) > 1 {
|
|
lang = matches[1]
|
|
}
|
|
|
|
return &ast.StyleNode{
|
|
Content: content,
|
|
Lang: lang,
|
|
Scoped: strings.Contains(p.source, "<style scoped") || strings.Contains(p.source, "<style lang=\"scss\" scoped"),
|
|
}, nil
|
|
}
|
|
|
|
func isWhitespace(c byte) bool {
|
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
|
|
}
|