Skip to content

Commit 381050b

Browse files
Major updates to Caddyfile lexer (#932)
* Major updates to Caddyfile lexer * yaml editorconfig * nolint false positive
1 parent e9292e6 commit 381050b

File tree

4 files changed

+423
-75
lines changed

4 files changed

+423
-75
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ insert_final_newline = true
1111
indent_style = space
1212
indent_size = 2
1313
insert_final_newline = false
14+
15+
[*.yml]
16+
indent_style = space
17+
indent_size = 2

lexers/caddyfile.go

Lines changed: 120 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,82 @@ import (
44
. "github.com/alecthomas/chroma/v2" // nolint
55
)
66

7+
// Matcher token stub for docs, or
8+
// Named matcher: @name, or
9+
// Path matcher: /foo, or
10+
// Wildcard path matcher: *
11+
// nolint: gosec
12+
var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
13+
14+
// Comment at start of line, or
15+
// Comment preceded by whitespace
16+
var caddyfileCommentRegexp = `(^|\s+)#.*\n`
17+
718
// caddyfileCommon are the rules common to both of the lexer variants
819
func caddyfileCommonRules() Rules {
920
return Rules{
1021
"site_block_common": {
22+
Include("site_body"),
23+
// Any other directive
24+
{`[^\s#]+`, Keyword, Push("directive")},
25+
Include("base"),
26+
},
27+
"site_body": {
1128
// Import keyword
12-
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
29+
{`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
1330
// Matcher definition
1431
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
1532
// Matcher token stub for docs
1633
{`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
1734
// These cannot have matchers but may have things that look like
1835
// matchers in their arguments, so we just parse as a subdirective.
19-
{`try_files`, Keyword, Push("subdirective")},
36+
{`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
2037
// These are special, they can nest more directives
21-
{`handle_errors|handle|route|handle_path|not`, Keyword, Push("nested_directive")},
22-
// Any other directive
23-
{`[^\s#]+`, Keyword, Push("directive")},
24-
Include("base"),
38+
{`\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Keyword, Push("nested_directive")},
39+
// uri directive has special syntax
40+
{`\b(uri)\b`, Keyword, Push("uri_directive")},
2541
},
2642
"matcher": {
2743
{`\{`, Punctuation, Push("block")},
2844
// Not can be one-liner
2945
{`not`, Keyword, Push("deep_not_matcher")},
46+
// Heredoc for CEL expression
47+
Include("heredoc"),
48+
// Backtick for CEL expression
49+
{"`", StringBacktick, Push("backticks")},
3050
// Any other same-line matcher
3151
{`[^\s#]+`, Keyword, Push("arguments")},
3252
// Terminators
33-
{`\n`, Text, Pop(1)},
53+
{`\s*\n`, Text, Pop(1)},
3454
{`\}`, Punctuation, Pop(1)},
3555
Include("base"),
3656
},
3757
"block": {
3858
{`\}`, Punctuation, Pop(2)},
59+
// Using double quotes doesn't stop at spaces
60+
{`"`, StringDouble, Push("double_quotes")},
61+
// Using backticks doesn't stop at spaces
62+
{"`", StringBacktick, Push("backticks")},
3963
// Not can be one-liner
4064
{`not`, Keyword, Push("not_matcher")},
41-
// Any other subdirective
65+
// Directives & matcher definitions
66+
Include("site_body"),
67+
// Any directive
4268
{`[^\s#]+`, Keyword, Push("subdirective")},
4369
Include("base"),
4470
},
4571
"nested_block": {
4672
{`\}`, Punctuation, Pop(2)},
47-
// Matcher definition
48-
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
49-
// Something that starts with literally < is probably a docs stub
50-
{`\<[^#]+\>`, Keyword, Push("nested_directive")},
51-
// Any other directive
52-
{`[^\s#]+`, Keyword, Push("nested_directive")},
73+
// Using double quotes doesn't stop at spaces
74+
{`"`, StringDouble, Push("double_quotes")},
75+
// Using backticks doesn't stop at spaces
76+
{"`", StringBacktick, Push("backticks")},
77+
// Not can be one-liner
78+
{`not`, Keyword, Push("not_matcher")},
79+
// Directives & matcher definitions
80+
Include("site_body"),
81+
// Any other subdirective
82+
{`[^\s#]+`, Keyword, Push("directive")},
5383
Include("base"),
5484
},
5585
"not_matcher": {
@@ -66,69 +96,97 @@ func caddyfileCommonRules() Rules {
6696
},
6797
"directive": {
6898
{`\{(?=\s)`, Punctuation, Push("block")},
69-
Include("matcher_token"),
70-
Include("comments_pop_1"),
71-
{`\n`, Text, Pop(1)},
99+
{caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
100+
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
101+
{`\s*\n`, Text, Pop(1)},
72102
Include("base"),
73103
},
74104
"nested_directive": {
75105
{`\{(?=\s)`, Punctuation, Push("nested_block")},
76-
Include("matcher_token"),
77-
Include("comments_pop_1"),
78-
{`\n`, Text, Pop(1)},
106+
{caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
107+
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
108+
{`\s*\n`, Text, Pop(1)},
79109
Include("base"),
80110
},
81111
"subdirective": {
82112
{`\{(?=\s)`, Punctuation, Push("block")},
83-
Include("comments_pop_1"),
84-
{`\n`, Text, Pop(1)},
113+
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
114+
{`\s*\n`, Text, Pop(1)},
85115
Include("base"),
86116
},
87117
"arguments": {
88118
{`\{(?=\s)`, Punctuation, Push("block")},
89-
Include("comments_pop_2"),
119+
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
90120
{`\\\n`, Text, nil}, // Skip escaped newlines
91-
{`\n`, Text, Pop(2)},
121+
{`\s*\n`, Text, Pop(2)},
122+
Include("base"),
123+
},
124+
"nested_arguments": {
125+
{`\{(?=\s)`, Punctuation, Push("nested_block")},
126+
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
127+
{`\\\n`, Text, nil}, // Skip escaped newlines
128+
{`\s*\n`, Text, Pop(2)},
92129
Include("base"),
93130
},
94131
"deep_subdirective": {
95132
{`\{(?=\s)`, Punctuation, Push("block")},
96-
Include("comments_pop_3"),
97-
{`\n`, Text, Pop(3)},
133+
{caddyfileCommentRegexp, CommentSingle, Pop(3)},
134+
{`\s*\n`, Text, Pop(3)},
135+
Include("base"),
136+
},
137+
"uri_directive": {
138+
{`\{(?=\s)`, Punctuation, Push("block")},
139+
{caddyfileMatcherTokenRegexp, NameDecorator, nil},
140+
{`(strip_prefix|strip_suffix|replace|path_regexp)`, NameConstant, Push("arguments")},
141+
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
142+
{`\s*\n`, Text, Pop(1)},
98143
Include("base"),
99144
},
100-
"matcher_token": {
101-
{`@[^\s]+`, NameDecorator, Push("arguments")}, // Named matcher
102-
{`/[^\s]+`, NameDecorator, Push("arguments")}, // Path matcher
103-
{`\*`, NameDecorator, Push("arguments")}, // Wildcard path matcher
104-
{`\[\<matcher\>\]`, NameDecorator, Push("arguments")}, // Matcher token stub for docs
145+
"double_quotes": {
146+
Include("placeholder"),
147+
{`\\"`, StringDouble, nil},
148+
{`[^"]`, StringDouble, nil},
149+
{`"`, StringDouble, Pop(1)},
105150
},
106-
"comments": {
107-
{`^#.*\n`, CommentSingle, nil}, // Comment at start of line
108-
{`\s+#.*\n`, CommentSingle, nil}, // Comment preceded by whitespace
151+
"backticks": {
152+
Include("placeholder"),
153+
{"\\\\`", StringBacktick, nil},
154+
{"[^`]", StringBacktick, nil},
155+
{"`", StringBacktick, Pop(1)},
109156
},
110-
"comments_pop_1": {
111-
{`^#.*\n`, CommentSingle, Pop(1)}, // Comment at start of line
112-
{`\s+#.*\n`, CommentSingle, Pop(1)}, // Comment preceded by whitespace
157+
"optional": {
158+
// Docs syntax for showing optional parts with [ ]
159+
{`\[`, Punctuation, Push("optional")},
160+
Include("name_constants"),
161+
{`\|`, Punctuation, nil},
162+
{`[^\[\]\|]+`, String, nil},
163+
{`\]`, Punctuation, Pop(1)},
113164
},
114-
"comments_pop_2": {
115-
{`^#.*\n`, CommentSingle, Pop(2)}, // Comment at start of line
116-
{`\s+#.*\n`, CommentSingle, Pop(2)}, // Comment preceded by whitespace
165+
"heredoc": {
166+
{`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
117167
},
118-
"comments_pop_3": {
119-
{`^#.*\n`, CommentSingle, Pop(3)}, // Comment at start of line
120-
{`\s+#.*\n`, CommentSingle, Pop(3)}, // Comment preceded by whitespace
168+
"name_constants": {
169+
{`\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, ByGroups(NameConstant, Punctuation), nil},
170+
},
171+
"placeholder": {
172+
// Placeholder with dots, colon for default value, brackets for args[0:]
173+
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, nil},
174+
// Handle opening brackets with no matching closing one
175+
{`\{[^\}\s]*\b`, String, nil},
121176
},
122177
"base": {
123-
Include("comments"),
124-
{`(on|off|first|last|before|after|internal|strip_prefix|strip_suffix|replace)\b`, NameConstant, nil},
125-
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)`, ByGroups(Name, Name, Punctuation, LiteralNumberInteger), nil},
126-
{`[a-z-]+/[a-z-+]+`, LiteralString, nil},
127-
{`[0-9]+[km]?\b`, LiteralNumberInteger, nil},
128-
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil}, // Placeholder
129-
{`\[(?=[^#{}$]+\])`, Punctuation, nil},
130-
{`\]|\|`, Punctuation, nil},
131-
{`[^\s#{}$\]]+`, LiteralString, nil},
178+
{caddyfileCommentRegexp, CommentSingle, nil},
179+
{`\[\<matcher\>\]`, NameDecorator, nil},
180+
Include("name_constants"),
181+
Include("heredoc"),
182+
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, ByGroups(Name, Name, Punctuation, NumberInteger, Name), nil},
183+
{`\[`, Punctuation, Push("optional")},
184+
{"`", StringBacktick, Push("backticks")},
185+
{`"`, StringDouble, Push("double_quotes")},
186+
Include("placeholder"),
187+
{`[a-z-]+/[a-z-+]+`, String, nil},
188+
{`[0-9]+([smhdk]|ns|us|µs|ms)?\b`, NumberInteger, nil},
189+
{`[^\s\n#\{]+`, String, nil},
132190
{`/[^\s#]*`, Name, nil},
133191
{`\s+`, Text, nil},
134192
},
@@ -149,27 +207,29 @@ var Caddyfile = Register(MustNewLexer(
149207
func caddyfileRules() Rules {
150208
return Rules{
151209
"root": {
152-
Include("comments"),
210+
{caddyfileCommentRegexp, CommentSingle, nil},
153211
// Global options block
154212
{`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
213+
// Top level import
214+
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
155215
// Snippets
156-
{`(\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
216+
{`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
157217
// Site label
158218
{`[^#{(\s,]+`, GenericHeading, Push("label")},
159219
// Site label with placeholder
160-
{`\{[\w+.\$-]+\}`, LiteralStringEscape, Push("label")},
220+
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
161221
{`\s+`, Text, nil},
162222
},
163223
"globals": {
164224
{`\}`, Punctuation, Pop(1)},
165-
{`[^\s#]+`, Keyword, Push("directive")},
225+
// Global options are parsed as subdirectives (no matcher)
226+
{`[^\s#]+`, Keyword, Push("subdirective")},
166227
Include("base"),
167228
},
168229
"snippet": {
169230
{`\}`, Punctuation, Pop(1)},
170-
// Matcher definition
171-
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
172-
// Any directive
231+
Include("site_body"),
232+
// Any other directive
173233
{`[^\s#]+`, Keyword, Push("directive")},
174234
Include("base"),
175235
},
@@ -179,7 +239,7 @@ func caddyfileRules() Rules {
179239
{`,\s*\n?`, Text, nil},
180240
{` `, Text, nil},
181241
// Site label with placeholder
182-
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil},
242+
Include("placeholder"),
183243
// Site label
184244
{`[^#{(\s,]+`, GenericHeading, nil},
185245
// Comment after non-block label (hack because comments end in \n)

lexers/testdata/caddyfile.actual

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,42 @@
44
on_demand_tls {
55
ask https://example.com
66
}
7+
log default {
8+
output file /var/log/caddy/access.log
9+
format json
10+
}
11+
auto_https disable_redirects
12+
renew_interval 20m
13+
14+
# this is a comment
15+
servers 192.168.1.2:8080 {
16+
name public
17+
trusted_proxies static private_ranges
18+
log_credentials
19+
}
720
}
821

9-
(blocking) {
22+
# top level comment
23+
24+
(blocking) {
1025
@blocked {
1126
path *.txt *.md *.mdown /site/*
1227
}
1328
redir @blocked /
1429
}
1530

31+
http://example.com {
32+
respond "http"
33+
}
34+
1635
example.com, fake.org, {$ENV_SITE} {
1736
root * /srv
1837

1938
respond /get-env {$ENV_VAR}
39+
respond /get-env {$ENV_VAR:default}
2040

21-
tls off
41+
tls internal
42+
tls /path/to/cert.pem /path/to/key.pem
2243

2344
route {
2445
# Add trailing slash for directory requests
@@ -67,6 +88,55 @@ example.com, fake.org, {$ENV_SITE} {
6788
respond @singleLine "Awesome."
6889

6990
import blocking
91+
import blocking foo
92+
import glob/*
7093

7194
file_server
72-
}
95+
96+
@named host example.com
97+
handle @named {
98+
handle /foo* {
99+
handle /foo* {
100+
respond "{path} foo"
101+
}
102+
}
103+
respond "foo"
104+
}
105+
106+
handle_path /foo* {
107+
respond "foo"
108+
}
109+
110+
reverse_proxy /api/* unix//var/run/api.sock {
111+
@good status 200
112+
handle_response @good {
113+
rewrite * /foo{uri}
114+
file_server
115+
}
116+
}
117+
118+
respond <<HTML
119+
<!DOCTYPE html>
120+
<html>
121+
<head>
122+
<title>Test</title>
123+
</head>
124+
<body>
125+
<h1>Hello, world!</h1>
126+
</body>
127+
</html>
128+
HTML 200
129+
130+
@file `file()`
131+
@first `file({'try_files': [{path}, {path} + '/', 'index.html']})`
132+
@smallest `file({'try_policy': 'smallest_size', 'try_files': ['a.txt', 'b.txt']})`
133+
134+
@without-both {
135+
not {
136+
path /api/*
137+
method POST
138+
}
139+
}
140+
141+
path_regexp [<name>] <regexp>
142+
}

0 commit comments

Comments
 (0)