2017-11-26 01:49:22 +01:00
|
|
|
package treerack
|
|
|
|
|
|
|
|
|
|
import (
|
2017-12-29 21:23:45 +01:00
|
|
|
"bytes"
|
2017-11-26 01:49:22 +01:00
|
|
|
"fmt"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
2017-12-29 21:23:45 +01:00
|
|
|
type formatDefinitionTestItem struct {
|
|
|
|
|
title string
|
|
|
|
|
definition string
|
2017-12-29 21:29:21 +01:00
|
|
|
syntax string
|
2017-12-29 21:23:45 +01:00
|
|
|
output string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDefinitionFormatItem(t *testing.T, treerack *Syntax, f formatFlags, test formatDefinitionTestItem) func(t *testing.T) {
|
|
|
|
|
return func(t *testing.T) {
|
2017-12-29 21:29:21 +01:00
|
|
|
syntax := test.syntax
|
|
|
|
|
if test.definition != "" {
|
|
|
|
|
syntax = fmt.Sprintf("def = %s", test.definition)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodes, err := treerack.Parse(bytes.NewBufferString(syntax))
|
2017-12-29 21:23:45 +01:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s := &Syntax{}
|
|
|
|
|
if err := define(s, nodes); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 03:53:20 +01:00
|
|
|
def, ok := s.registry.definition["def"]
|
2017-12-29 21:23:45 +01:00
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("failed to register definition")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 20:14:24 +02:00
|
|
|
output := def.format(s.registry, formatOptions{mode: f})
|
2017-12-29 21:23:45 +01:00
|
|
|
if output != test.output {
|
|
|
|
|
t.Error("invalid definition format")
|
|
|
|
|
t.Log("got: ", output)
|
|
|
|
|
t.Log("expected:", test.output)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDefinitionFormat(t *testing.T, f formatFlags, tests []formatDefinitionTestItem) {
|
|
|
|
|
treerack, err := bootSyntax()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
2017-11-26 01:49:22 +01:00
|
|
|
}
|
|
|
|
|
|
2017-12-29 21:23:45 +01:00
|
|
|
for _, test := range tests {
|
|
|
|
|
t.Run(test.title, testDefinitionFormatItem(t, treerack, f, test))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCharFormat(t *testing.T) {
|
|
|
|
|
testDefinitionFormat(t, formatNone, []formatDefinitionTestItem{{
|
2017-11-26 01:49:22 +01:00
|
|
|
title: "empty",
|
|
|
|
|
definition: "[]",
|
|
|
|
|
output: "[]",
|
|
|
|
|
}, {
|
|
|
|
|
title: "one char",
|
|
|
|
|
definition: "[a]",
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "escaped char",
|
|
|
|
|
definition: "[\\a]",
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "escaped control char",
|
|
|
|
|
definition: "[\\^]",
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"^"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "escaped whitespace char",
|
|
|
|
|
definition: "[\\n]",
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"\n"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "escaped verbatim whitespace char",
|
|
|
|
|
definition: "[\n]",
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"\n"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "escaped range",
|
|
|
|
|
definition: "[\\b-\\v]",
|
|
|
|
|
output: "[\\b-\\v]",
|
|
|
|
|
}, {
|
|
|
|
|
title: "anything",
|
|
|
|
|
definition: ".",
|
|
|
|
|
output: ".",
|
|
|
|
|
}, {
|
|
|
|
|
title: "not something",
|
|
|
|
|
definition: "[^abc]",
|
|
|
|
|
output: "[^abc]",
|
|
|
|
|
}, {
|
|
|
|
|
title: "range",
|
|
|
|
|
definition: "[a-z]",
|
|
|
|
|
output: "[a-z]",
|
|
|
|
|
}, {
|
|
|
|
|
title: "range and char mixed",
|
|
|
|
|
definition: "[a-z_\\-A-Z]",
|
|
|
|
|
output: "[_\\-a-zA-Z]",
|
2017-12-29 21:23:45 +01:00
|
|
|
}})
|
2017-11-26 01:49:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSequenceFormat(t *testing.T) {
|
2017-12-29 21:29:21 +01:00
|
|
|
testDefinitionFormat(t, formatNone, []formatDefinitionTestItem{{
|
2017-11-26 01:49:22 +01:00
|
|
|
title: "empty char sequence",
|
|
|
|
|
syntax: `def = ""`,
|
|
|
|
|
output: `""`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "char sequence",
|
|
|
|
|
syntax: `def = "abc"`,
|
|
|
|
|
output: `"abc"`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "char sequence, escaped",
|
|
|
|
|
syntax: `def = "\\n"`,
|
|
|
|
|
output: `"\\n"`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "chars",
|
|
|
|
|
syntax: `def = "abc" [a-z]`,
|
|
|
|
|
output: `"abc" [a-z]`,
|
|
|
|
|
}, {
|
2017-12-29 21:23:45 +01:00
|
|
|
title: "quantifiers, 0-or-more, single char",
|
2017-11-26 01:49:22 +01:00
|
|
|
syntax: `def = "a"*`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"*`,
|
2017-12-29 21:23:45 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, 0-or-more",
|
|
|
|
|
syntax: `def = "abc"*`,
|
|
|
|
|
output: `"abc"*`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, 1-or-more, single char",
|
|
|
|
|
syntax: `def = "a"+`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"+`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, 1-or-more",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"+`,
|
|
|
|
|
output: `"abc"+`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, 0-or-one, single char",
|
|
|
|
|
syntax: `def = "a"?`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"?`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, 0-or-one",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"?`,
|
|
|
|
|
output: `"abc"?`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, exact number, single char",
|
|
|
|
|
syntax: `def = "a"{3}`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"{3}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, exact number",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"{3}`,
|
|
|
|
|
output: `"abc"{3}`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, max, single char",
|
|
|
|
|
syntax: `def = "a"{0, 3}`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"{,3}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, max",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"{0, 3}`,
|
|
|
|
|
output: `"abc"{,3}`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, min, single char",
|
|
|
|
|
syntax: `def = "a"{3,}`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"{3,}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, min",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"{3,}`,
|
|
|
|
|
output: `"abc"{3,}`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, range, single char",
|
|
|
|
|
syntax: `def = "a"{3, 9}`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a"{3,9}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "quantifiers, range",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc"{3, 9}`,
|
|
|
|
|
output: `"abc"{3,9}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "symbols",
|
|
|
|
|
syntax: `a = "a"; b = "b"; c = "c"; def = a b c`,
|
|
|
|
|
output: "a b c",
|
|
|
|
|
}, {
|
2017-12-29 21:23:45 +01:00
|
|
|
title: "choice in sequence, single char",
|
2017-11-26 01:49:22 +01:00
|
|
|
syntax: `def = "a" ("b" | "c")`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a" ("b" | "c")`,
|
2017-12-29 21:23:45 +01:00
|
|
|
}, {
|
|
|
|
|
title: "choice in sequence",
|
|
|
|
|
syntax: `def = "abc" ("def" | "ghi")`,
|
|
|
|
|
output: `"abc" ("def" | "ghi")`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "grouped quantifier, single char",
|
|
|
|
|
syntax: `def = ("a" "b"){3}`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `("a" "b"){3}`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "grouped quantifier",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = ("abc" "def"){3}`,
|
|
|
|
|
output: `("abc" "def"){3}`,
|
2017-12-29 21:29:21 +01:00
|
|
|
}})
|
2017-11-26 01:49:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestChoiceFormat(t *testing.T) {
|
2017-12-29 21:29:21 +01:00
|
|
|
testDefinitionFormat(t, formatNone, []formatDefinitionTestItem{{
|
2017-12-29 21:23:45 +01:00
|
|
|
title: "choice of char sequences, single char",
|
2017-11-26 01:49:22 +01:00
|
|
|
syntax: `def = "a" | "b" | "c"`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a" | "b" | "c"`,
|
2017-12-29 21:23:45 +01:00
|
|
|
}, {
|
|
|
|
|
title: "choice of char sequences",
|
|
|
|
|
syntax: `def = "abc" | "def" | "ghi"`,
|
|
|
|
|
output: `"abc" | "def" | "ghi"`,
|
|
|
|
|
}, {
|
|
|
|
|
title: "choice of inline sequences, single char",
|
|
|
|
|
syntax: `def = "a" "b" | "c" "d" | "e" "f"`,
|
2026-05-30 20:14:24 +02:00
|
|
|
output: `"a" "b" | "c" "d" | "e" "f"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "choice of inline sequences",
|
2017-12-29 21:23:45 +01:00
|
|
|
syntax: `def = "abc" "def" | "ghi" "jkl" | "mno" "pqr"`,
|
|
|
|
|
output: `"abc" "def" | "ghi" "jkl" | "mno" "pqr"`,
|
2017-11-26 01:49:22 +01:00
|
|
|
}, {
|
|
|
|
|
title: "choice of symbol",
|
|
|
|
|
syntax: `a = "a"; b = "b"; c = "c"; def = a | b | c`,
|
|
|
|
|
output: "a | b | c",
|
2017-12-29 21:29:21 +01:00
|
|
|
}})
|
2017-11-26 01:49:22 +01:00
|
|
|
}
|
2017-12-29 21:23:45 +01:00
|
|
|
|
2026-05-30 20:14:24 +02:00
|
|
|
const testDoc = `/*
|
|
|
|
|
foo
|
|
|
|
|
*/
|
2026-06-01 20:28:39 +02:00
|
|
|
// bar
|
|
|
|
|
// bar
|
|
|
|
|
//baz
|
|
|
|
|
/* foo
|
|
|
|
|
bar baz */// foo bar baz
|
|
|
|
|
wschar:alias =// foo
|
|
|
|
|
/* bar */ " " | "\t" | "\n" | "\b" | "\f" | "\r" | "\v";
|
|
|
|
|
wsc:ws = wschar | comment;
|
|
|
|
|
|
|
|
|
|
block-comment:alias:nows /* foo */ // bar
|
|
|
|
|
= "/*" ("*" [^/] | [^*])* "*/";
|
|
|
|
|
line-comment:alias:nows /*
|
|
|
|
|
foo
|
|
|
|
|
*/ = "//" [^\n]*;
|
|
|
|
|
comment-segment:alias:nows = // bar
|
|
|
|
|
line-comment | block-comment;
|
|
|
|
|
ws-no-nl:alias:nows = " " | "\t" | "\b" | /* this one */ /* is a */ "\f" /* form feed */ // for sure
|
|
|
|
|
| "\r" | "\v";
|
|
|
|
|
comment:nows = comment-segment /* segment is not the best name */ /* but */ (ws-no-nl* "\n"? ws-no-nl* // fine
|
|
|
|
|
comment-segment)*;
|
|
|
|
|
|
|
|
|
|
any-char = "."; // equivalent to [^]
|
|
|
|
|
|
|
|
|
|
// caution: newline is accepted
|
|
|
|
|
/* class not */ class-not = "^";
|
|
|
|
|
class-char:nows = [^\\\[\]\^\-] | "\\" . /* foo
|
|
|
|
|
bar */;
|
|
|
|
|
char-range:nows = class-char "-" class-char // foo
|
|
|
|
|
;
|
|
|
|
|
char-class:nows = "[" class-not? (class-char | char-range)* "]"; // foo
|
|
|
|
|
/* bar
|
|
|
|
|
baz */
|
|
|
|
|
|
|
|
|
|
// newline is accepted
|
|
|
|
|
sequence-char:nows = [^\\"] | "\\" .;
|
|
|
|
|
char-sequence:nows = "\"" sequence-char* "\"";
|
|
|
|
|
|
|
|
|
|
terminal:alias = any-char | char-class | char-sequence;
|
|
|
|
|
|
|
|
|
|
symbol:nows = [^\\ \n\t\b\f\r\v/.\[\]\"{}\^+*?|():=;]+;
|
|
|
|
|
|
|
|
|
|
group:alias = "(" expression ")";
|
|
|
|
|
|
|
|
|
|
number:alias:nows = [0-9]+;
|
|
|
|
|
count = number;
|
|
|
|
|
count-quantifier = "{" count "}";
|
|
|
|
|
range-from = number;
|
|
|
|
|
range-to = number;
|
|
|
|
|
range-quantifier = "{" range-from? "," range-to? "}";
|
|
|
|
|
one-or-more = "+";
|
|
|
|
|
zero-or-more = "*";
|
|
|
|
|
zero-or-one = "?";
|
|
|
|
|
quantity:alias = count-quantifier
|
|
|
|
|
| range-quantifier
|
|
|
|
|
| one-or-more
|
|
|
|
|
| zero-or-more
|
|
|
|
|
| zero-or-one;
|
|
|
|
|
|
|
|
|
|
item:nows = (terminal | symbol | group) quantity?;
|
|
|
|
|
sequence = item+;
|
|
|
|
|
|
|
|
|
|
option:alias = terminal | symbol | group | sequence;
|
|
|
|
|
|
|
|
|
|
// DOC: how the order matters
|
|
|
|
|
choice = option ("|" option)+;
|
|
|
|
|
|
|
|
|
|
// DOC: not having 'not' needs some tricks sometimes
|
|
|
|
|
|
|
|
|
|
expression:alias = terminal | symbol | group | sequence | choice;
|
|
|
|
|
|
|
|
|
|
alias = "alias";
|
|
|
|
|
ws = "ws";
|
|
|
|
|
nows = "nows";
|
|
|
|
|
kw = "kw";
|
|
|
|
|
nokw = "nokw";
|
|
|
|
|
failpass = "failpass";
|
|
|
|
|
root = "root";
|
|
|
|
|
flag:alias = alias | ws | nows | kw | nokw | failpass | root;
|
|
|
|
|
definition-name:alias:nows = symbol (":" flag)*;
|
|
|
|
|
definition = definition-name "=" expression;
|
|
|
|
|
|
|
|
|
|
definitions:alias = definition (";"+ definition)*;
|
|
|
|
|
syntax:root = ";"* definitions? ";"*;
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
const testDocCheck = `/*
|
|
|
|
|
foo
|
|
|
|
|
*/
|
2026-05-30 20:14:24 +02:00
|
|
|
// bar
|
|
|
|
|
// bar
|
|
|
|
|
// baz
|
|
|
|
|
/* foo
|
|
|
|
|
bar baz */ // foo bar baz
|
2026-06-01 20:28:39 +02:00
|
|
|
wschar:alias = // foo
|
|
|
|
|
/* bar */
|
|
|
|
|
" " | "\t" | "\n" | "\b" | "\f" | "\r" | "\v";
|
2026-05-30 20:14:24 +02:00
|
|
|
wsc:ws = wschar | comment;
|
2026-06-01 20:28:39 +02:00
|
|
|
block-comment:alias:nows = /* foo */ // bar
|
|
|
|
|
"/*" ("*" [^/] | [^*])* "*/";
|
|
|
|
|
line-comment:alias:nows = /*
|
|
|
|
|
foo
|
|
|
|
|
*/
|
|
|
|
|
"//" [^\n]*;
|
|
|
|
|
comment-segment:alias:nows = // bar
|
|
|
|
|
line-comment | block-comment;
|
2026-05-30 20:14:24 +02:00
|
|
|
ws-no-nl:alias:nows = " "
|
|
|
|
|
| "\t"
|
|
|
|
|
| "\b"
|
|
|
|
|
/* this one */ /* is a */
|
|
|
|
|
| "\f"
|
2026-06-01 20:28:39 +02:00
|
|
|
/* form feed */ // for sure
|
2026-05-30 20:14:24 +02:00
|
|
|
| "\r"
|
|
|
|
|
| "\v";
|
|
|
|
|
comment:nows = comment-segment
|
|
|
|
|
/* segment is not the best name */ /* but */
|
2026-06-01 20:28:39 +02:00
|
|
|
( ws-no-nl*
|
|
|
|
|
"\n"?
|
|
|
|
|
ws-no-nl*
|
|
|
|
|
// fine
|
|
|
|
|
comment-segment
|
|
|
|
|
)*;
|
2026-05-30 20:14:24 +02:00
|
|
|
any-char = "."; // equivalent to [^]
|
2017-12-29 21:23:45 +01:00
|
|
|
|
2026-05-30 20:14:24 +02:00
|
|
|
// caution: newline is accepted
|
2026-06-01 20:28:39 +02:00
|
|
|
/* class not */
|
2026-05-30 20:14:24 +02:00
|
|
|
class-not = "^";
|
2026-06-01 20:28:39 +02:00
|
|
|
class-char:nows = [^\\\[\]\^\-] | "\\" .; /* foo
|
|
|
|
|
bar */
|
|
|
|
|
char-range:nows = class-char "-" class-char; // foo
|
|
|
|
|
char-class:nows = "[" class-not? (class-char | char-range)* "]"; // foo
|
|
|
|
|
/* bar
|
|
|
|
|
baz */
|
2026-05-30 20:14:24 +02:00
|
|
|
|
|
|
|
|
// newline is accepted
|
|
|
|
|
sequence-char:nows = [^\\"] | "\\" .;
|
|
|
|
|
char-sequence:nows = "\"" sequence-char* "\"";
|
|
|
|
|
terminal:alias = any-char | char-class | char-sequence;
|
|
|
|
|
symbol:nows = [^\\ \n\t\b\f\r\v/.\[\]\"{}\^+*?|():=;]+;
|
|
|
|
|
group:alias = "(" expression ")";
|
|
|
|
|
number:alias:nows = [0-9]+;
|
|
|
|
|
count = number;
|
|
|
|
|
count-quantifier = "{" count "}";
|
|
|
|
|
range-from = number;
|
|
|
|
|
range-to = number;
|
|
|
|
|
range-quantifier = "{" range-from? "," range-to? "}";
|
|
|
|
|
one-or-more = "+";
|
|
|
|
|
zero-or-more = "*";
|
|
|
|
|
zero-or-one = "?";
|
|
|
|
|
quantity:alias = count-quantifier | range-quantifier | one-or-more | zero-or-more | zero-or-one;
|
|
|
|
|
item:nows = (terminal | symbol | group) quantity?;
|
|
|
|
|
sequence = item+;
|
|
|
|
|
option:alias = terminal | symbol | group | sequence;
|
|
|
|
|
|
|
|
|
|
// DOC: how the order matters
|
|
|
|
|
choice = option ("|" option)+;
|
|
|
|
|
|
|
|
|
|
// DOC: not having 'not' needs some tricks sometimes
|
|
|
|
|
|
|
|
|
|
expression:alias = terminal | symbol | group | sequence | choice;
|
|
|
|
|
alias = "alias";
|
|
|
|
|
ws = "ws";
|
|
|
|
|
nows = "nows";
|
|
|
|
|
kw = "kw";
|
|
|
|
|
nokw = "nokw";
|
|
|
|
|
failpass = "failpass";
|
|
|
|
|
root = "root";
|
|
|
|
|
flag:alias = alias | ws | nows | kw | nokw | failpass | root;
|
|
|
|
|
definition-name:alias:nows = symbol (":" flag)*;
|
|
|
|
|
definition = definition-name "=" expression;
|
|
|
|
|
definitions:alias = definition (";"+ definition)*;
|
2026-06-01 21:29:33 +02:00
|
|
|
syntax:root = ";"* definitions? ";"*;
|
|
|
|
|
`
|
2026-05-30 20:14:24 +02:00
|
|
|
|
2026-06-05 18:08:33 +02:00
|
|
|
const testDocURL = `// basd on RFC3986 and RFC6874
|
|
|
|
|
|
|
|
|
|
// char types:
|
|
|
|
|
digit:alias:failpass = [0-9];
|
|
|
|
|
hex:alias:failpass = [0-9a-fA-F];
|
|
|
|
|
alpha:alias:failpass = [a-zA-Z];
|
|
|
|
|
delimiter:alias:failpass = ":" | "/" | "?" | "#" | "[" | "]" | "@";
|
|
|
|
|
subdelimiter:alias:failpass = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=";
|
|
|
|
|
unreserved:alias:failpass = alpha | digit | "-" | "." | "_" | "~";
|
|
|
|
|
reserved:alias:failpass = delimiter | subdelimiter;
|
|
|
|
|
percent-encoded:alias:failpass = "%" hex{2};
|
|
|
|
|
path-char:alias:failpass = unreserved | percent-encoded | subdelimiter | ":" | "@";
|
|
|
|
|
|
|
|
|
|
// scheme:
|
|
|
|
|
scheme = alpha (alpha | digit | [+-.])*;
|
|
|
|
|
|
|
|
|
|
// userinfo:
|
|
|
|
|
userinfo = (unreserved | percent-encoded | subdelimiter | ":")*;
|
|
|
|
|
|
|
|
|
|
// IPv4:
|
|
|
|
|
dec-byte:alias:failpass = digit | [1-9] digit | "1" digit{2} | "2" [0-4] digit | "25" [0-5];
|
|
|
|
|
ipv4:failpass = dec-byte ("." dec-byte){3};
|
|
|
|
|
|
|
|
|
|
// IPv6:
|
|
|
|
|
h16:alias:failpass = hex{1,4};
|
|
|
|
|
ls32:alias:failpass = (h16 ":" h16) | ipv4;
|
|
|
|
|
ipv6:failpass = (h16 ":"){6} ls32
|
|
|
|
|
| "::" (h16 ":"){5} ls32
|
|
|
|
|
| h16? "::" (h16 ":"){4} ls32
|
|
|
|
|
| ((h16 ":")? h16)? "::" (h16 ":"){3} ls32
|
|
|
|
|
| ((h16 ":"){,2} h16)? "::" (h16 ":"){2} ls32
|
|
|
|
|
| ((h16 ":"){,3} h16)? "::" h16 ":" ls32
|
|
|
|
|
| ((h16 ":"){,4} h16)? "::" ls32
|
|
|
|
|
| ((h16 ":"){,5} h16)? "::" h16
|
|
|
|
|
| ((h16 ":"){,6} h16)? "::" ;
|
|
|
|
|
zone-id:alias:failpass = (unreserved | percent-encoded)+;
|
|
|
|
|
ipv6-zone:failpass = ipv6 "%25" zone-id; // RFC6874
|
|
|
|
|
|
|
|
|
|
// host:
|
|
|
|
|
registry-name-rfc:failpass = (unreserved | percent-encoded | subdelimiter)*; // all RFC chars allowed
|
|
|
|
|
dns-label:alias:failpass = (alpha | digit) ("-"* (alpha | digit)+)*;
|
|
|
|
|
domain-name:failpass = dns-label ("." dns-label)* "."?; // DNS compatible
|
|
|
|
|
hostname-rfc = ipv4 | "[" (ipv6 | ipv6-zone) "]" | registry-name-rfc;
|
|
|
|
|
hostname = ipv4 | "[" (ipv6 | ipv6-zone) "]" | domain-name;
|
|
|
|
|
port = digit*;
|
|
|
|
|
host-rfc:alias:failpass = hostname-rfc (":" port)?;
|
|
|
|
|
host:alias:failpass = hostname (":" port)?;
|
|
|
|
|
|
|
|
|
|
// path:
|
|
|
|
|
segment:alias:failpass = path-char*;
|
|
|
|
|
segment-non-zero:alias:failpass = path-char+;
|
|
|
|
|
segment-non-zero-no-colon:alias:failpass = (unreserved | percent-encoded | subdelimiter | "@")+;
|
|
|
|
|
path-absolute-or-empty = ("/" segment)*;
|
|
|
|
|
path-absolute = "/" (segment-non-zero ("/" segment)*)?;
|
|
|
|
|
path-rootless = segment-non-zero ("/" segment)*;
|
|
|
|
|
path-noscheme = segment-non-zero-no-colon ("/" segment)*;
|
|
|
|
|
path-empty = "";
|
|
|
|
|
query = (path-char | "/" | "?")*;
|
|
|
|
|
fragment = (path-char | "/" | "?")*;
|
|
|
|
|
|
|
|
|
|
// composed together:
|
|
|
|
|
authority-rfc:alias:failpass = (userinfo "@")? host-rfc;
|
|
|
|
|
authority:alias:failpass = (userinfo "@")? host;
|
|
|
|
|
hierarchy-part-rfc:alias:failpass = "//" authority-rfc path-absolute-or-empty | path-absolute | path-rootless | path-empty;
|
|
|
|
|
hierarchy-part:alias:failpass = "//" authority path-absolute-or-empty | path-absolute | path-rootless | path-empty;
|
|
|
|
|
relative-part-rfc:alias:failpass = "//" authority-rfc path-absolute-or-empty | path-absolute | path-noscheme | path-empty;
|
|
|
|
|
relative-part:alias:failpass = "//" authority path-absolute-or-empty | path-absolute | path-noscheme | path-empty;
|
|
|
|
|
absolute-url-rfc = scheme ":" hierarchy-part-rfc ("?" query)? ("#" fragment)?;
|
|
|
|
|
absolute-url = scheme ":" hierarchy-part ("?" query)? ("#" fragment)?;
|
|
|
|
|
relative-url-rfc = relative-part-rfc ("?" query)? ("#" fragment)?;
|
|
|
|
|
relative-url = relative-part ("?" query)? ("#" fragment)?;
|
|
|
|
|
|
|
|
|
|
// supporting four possible types URLs:
|
|
|
|
|
url:root = absolute-url | absolute-url-rfc | relative-url | relative-url-rfc;
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
const testDocCheckURL = `// basd on RFC3986 and RFC6874
|
|
|
|
|
|
|
|
|
|
// char types:
|
|
|
|
|
digit:alias:failpass = [0-9];
|
|
|
|
|
hex:alias:failpass = [0-9a-fA-F];
|
|
|
|
|
alpha:alias:failpass = [a-zA-Z];
|
|
|
|
|
delimiter:alias:failpass = ":" | "/" | "?" | "#" | "[" | "]" | "@";
|
|
|
|
|
subdelimiter:alias:failpass = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=";
|
|
|
|
|
unreserved:alias:failpass = alpha | digit | "-" | "." | "_" | "~";
|
|
|
|
|
reserved:alias:failpass = delimiter | subdelimiter;
|
|
|
|
|
percent-encoded:alias:failpass = "%" hex{2};
|
|
|
|
|
path-char:alias:failpass = unreserved | percent-encoded | subdelimiter | ":" | "@";
|
|
|
|
|
|
|
|
|
|
// scheme:
|
|
|
|
|
scheme = alpha (alpha | digit | [+-.])*;
|
|
|
|
|
|
|
|
|
|
// userinfo:
|
|
|
|
|
userinfo = (unreserved | percent-encoded | subdelimiter | ":")*;
|
|
|
|
|
|
|
|
|
|
// IPv4:
|
|
|
|
|
dec-byte:alias:failpass = digit | [1-9] digit | "1" digit{2} | "2" [0-4] digit | "25" [0-5];
|
|
|
|
|
ipv4:failpass = dec-byte ("." dec-byte){3};
|
|
|
|
|
|
|
|
|
|
// IPv6:
|
|
|
|
|
h16:alias:failpass = hex{1,4};
|
|
|
|
|
ls32:alias:failpass = h16 ":" h16 | ipv4;
|
|
|
|
|
ipv6:failpass = (h16 ":"){6} ls32
|
|
|
|
|
| "::" (h16 ":"){5} ls32
|
|
|
|
|
| h16? "::" (h16 ":"){4} ls32
|
|
|
|
|
| ((h16 ":")? h16)? "::" (h16 ":"){3} ls32
|
|
|
|
|
| ((h16 ":"){,2} h16)? "::" (h16 ":"){2} ls32
|
|
|
|
|
| ((h16 ":"){,3} h16)? "::" h16 ":" ls32
|
|
|
|
|
| ((h16 ":"){,4} h16)? "::" ls32
|
|
|
|
|
| ((h16 ":"){,5} h16)? "::" h16
|
|
|
|
|
| ((h16 ":"){,6} h16)? "::";
|
|
|
|
|
zone-id:alias:failpass = (unreserved | percent-encoded)+;
|
|
|
|
|
ipv6-zone:failpass = ipv6 "%25" zone-id; // RFC6874
|
|
|
|
|
|
|
|
|
|
// host:
|
|
|
|
|
registry-name-rfc:failpass = (unreserved | percent-encoded | subdelimiter)*; // all RFC chars allowed
|
|
|
|
|
dns-label:alias:failpass = (alpha | digit) ("-"* (alpha | digit)+)*;
|
|
|
|
|
domain-name:failpass = dns-label ("." dns-label)* "."?; // DNS compatible
|
|
|
|
|
hostname-rfc = ipv4 | "[" (ipv6 | ipv6-zone) "]" | registry-name-rfc;
|
|
|
|
|
hostname = ipv4 | "[" (ipv6 | ipv6-zone) "]" | domain-name;
|
|
|
|
|
port = digit*;
|
|
|
|
|
host-rfc:alias:failpass = hostname-rfc (":" port)?;
|
|
|
|
|
host:alias:failpass = hostname (":" port)?;
|
|
|
|
|
|
|
|
|
|
// path:
|
|
|
|
|
segment:alias:failpass = path-char*;
|
|
|
|
|
segment-non-zero:alias:failpass = path-char+;
|
|
|
|
|
segment-non-zero-no-colon:alias:failpass = (unreserved | percent-encoded | subdelimiter | "@")+;
|
|
|
|
|
path-absolute-or-empty = ("/" segment)*;
|
|
|
|
|
path-absolute = "/" (segment-non-zero ("/" segment)*)?;
|
|
|
|
|
path-rootless = segment-non-zero ("/" segment)*;
|
|
|
|
|
path-noscheme = segment-non-zero-no-colon ("/" segment)*;
|
|
|
|
|
path-empty = "";
|
|
|
|
|
query = (path-char | "/" | "?")*;
|
|
|
|
|
fragment = (path-char | "/" | "?")*;
|
|
|
|
|
|
|
|
|
|
// composed together:
|
|
|
|
|
authority-rfc:alias:failpass = (userinfo "@")? host-rfc;
|
|
|
|
|
authority:alias:failpass = (userinfo "@")? host;
|
|
|
|
|
hierarchy-part-rfc:alias:failpass = "//" authority-rfc path-absolute-or-empty
|
|
|
|
|
| path-absolute
|
|
|
|
|
| path-rootless
|
|
|
|
|
| path-empty;
|
|
|
|
|
hierarchy-part:alias:failpass = "//" authority path-absolute-or-empty
|
|
|
|
|
| path-absolute
|
|
|
|
|
| path-rootless
|
|
|
|
|
| path-empty;
|
|
|
|
|
relative-part-rfc:alias:failpass = "//" authority-rfc path-absolute-or-empty
|
|
|
|
|
| path-absolute
|
|
|
|
|
| path-noscheme
|
|
|
|
|
| path-empty;
|
|
|
|
|
relative-part:alias:failpass = "//" authority path-absolute-or-empty
|
|
|
|
|
| path-absolute
|
|
|
|
|
| path-noscheme
|
|
|
|
|
| path-empty;
|
|
|
|
|
absolute-url-rfc = scheme ":" hierarchy-part-rfc ("?" query)? ("#" fragment)?;
|
|
|
|
|
absolute-url = scheme ":" hierarchy-part ("?" query)? ("#" fragment)?;
|
|
|
|
|
relative-url-rfc = relative-part-rfc ("?" query)? ("#" fragment)?;
|
|
|
|
|
relative-url = relative-part ("?" query)? ("#" fragment)?;
|
|
|
|
|
|
|
|
|
|
// supporting four possible types URLs:
|
|
|
|
|
url:root = absolute-url | absolute-url-rfc | relative-url | relative-url-rfc;
|
|
|
|
|
`
|
|
|
|
|
|
2026-05-30 20:14:24 +02:00
|
|
|
func TestDocFormat(t *testing.T) {
|
2026-06-01 20:28:39 +02:00
|
|
|
for _, test := range []struct{ title, in, out string }{{
|
|
|
|
|
title: "format",
|
|
|
|
|
in: testDoc,
|
|
|
|
|
out: testDocCheck,
|
|
|
|
|
}, {
|
|
|
|
|
title: "check",
|
|
|
|
|
in: testDocCheck,
|
|
|
|
|
out: testDocCheck,
|
2026-06-05 18:08:33 +02:00
|
|
|
}, {
|
|
|
|
|
title: "format url",
|
|
|
|
|
in: testDocURL,
|
|
|
|
|
out: testDocCheckURL,
|
|
|
|
|
}, {
|
|
|
|
|
title: "check url",
|
|
|
|
|
in: testDocCheckURL,
|
|
|
|
|
out: testDocCheckURL,
|
2026-06-01 20:28:39 +02:00
|
|
|
}} {
|
|
|
|
|
t.Run(test.title, func(t *testing.T) {
|
|
|
|
|
in := bytes.NewBufferString(test.in)
|
|
|
|
|
s := &Syntax{}
|
|
|
|
|
if err := s.ReadSyntax(in); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-05-30 20:14:24 +02:00
|
|
|
|
2026-06-01 20:28:39 +02:00
|
|
|
out := bytes.NewBuffer(nil)
|
|
|
|
|
if err := s.Format(out); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-05-30 20:14:24 +02:00
|
|
|
|
2026-06-01 20:28:39 +02:00
|
|
|
if out.String() != test.out {
|
|
|
|
|
t.Log(test.out)
|
|
|
|
|
t.Log(out.String())
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-05-30 20:14:24 +02:00
|
|
|
}
|
2017-12-29 21:23:45 +01:00
|
|
|
}
|