caddyfile.go

  1package lexers
  2
  3import (
  4	. "github.com/alecthomas/chroma/v2" // nolint
  5)
  6
  7// Matcher token stub for docs, or
  8// Named matcher: @name, or
  9// Path matcher: /foo, or
 10// Wildcard path matcher: *
 11// nolint: gosec
 12var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
 13
 14// Comment at start of line, or
 15// Comment preceded by whitespace
 16var caddyfileCommentRegexp = `(^|\s+)#.*\n`
 17
 18// caddyfileCommon are the rules common to both of the lexer variants
 19func caddyfileCommonRules() Rules {
 20	return Rules{
 21		"site_block_common": {
 22			Include("site_body"),
 23			// Any other directive
 24			{`[^\s#]+`, Keyword, Push("directive")},
 25			Include("base"),
 26		},
 27		"site_body": {
 28			// Import keyword
 29			{`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
 30			// Matcher definition
 31			{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
 32			// Matcher token stub for docs
 33			{`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
 34			// These cannot have matchers but may have things that look like
 35			// matchers in their arguments, so we just parse as a subdirective.
 36			{`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
 37			// These are special, they can nest more directives
 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")},
 41		},
 42		"matcher": {
 43			{`\{`, Punctuation, Push("block")},
 44			// Not can be one-liner
 45			{`not`, Keyword, Push("deep_not_matcher")},
 46			// Heredoc for CEL expression
 47			Include("heredoc"),
 48			// Backtick for CEL expression
 49			{"`", StringBacktick, Push("backticks")},
 50			// Any other same-line matcher
 51			{`[^\s#]+`, Keyword, Push("arguments")},
 52			// Terminators
 53			{`\s*\n`, Text, Pop(1)},
 54			{`\}`, Punctuation, Pop(1)},
 55			Include("base"),
 56		},
 57		"block": {
 58			{`\}`, 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")},
 63			// Not can be one-liner
 64			{`not`, Keyword, Push("not_matcher")},
 65			// Directives & matcher definitions
 66			Include("site_body"),
 67			// Any directive
 68			{`[^\s#]+`, Keyword, Push("subdirective")},
 69			Include("base"),
 70		},
 71		"nested_block": {
 72			{`\}`, Punctuation, Pop(2)},
 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")},
 83			Include("base"),
 84		},
 85		"not_matcher": {
 86			{`\}`, Punctuation, Pop(2)},
 87			{`\{(?=\s)`, Punctuation, Push("block")},
 88			{`[^\s#]+`, Keyword, Push("arguments")},
 89			{`\s+`, Text, nil},
 90		},
 91		"deep_not_matcher": {
 92			{`\}`, Punctuation, Pop(2)},
 93			{`\{(?=\s)`, Punctuation, Push("block")},
 94			{`[^\s#]+`, Keyword, Push("deep_subdirective")},
 95			{`\s+`, Text, nil},
 96		},
 97		"directive": {
 98			{`\{(?=\s)`, Punctuation, Push("block")},
 99			{caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
100			{caddyfileCommentRegexp, CommentSingle, Pop(1)},
101			{`\s*\n`, Text, Pop(1)},
102			Include("base"),
103		},
104		"nested_directive": {
105			{`\{(?=\s)`, Punctuation, Push("nested_block")},
106			{caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
107			{caddyfileCommentRegexp, CommentSingle, Pop(1)},
108			{`\s*\n`, Text, Pop(1)},
109			Include("base"),
110		},
111		"subdirective": {
112			{`\{(?=\s)`, Punctuation, Push("block")},
113			{caddyfileCommentRegexp, CommentSingle, Pop(1)},
114			{`\s*\n`, Text, Pop(1)},
115			Include("base"),
116		},
117		"arguments": {
118			{`\{(?=\s)`, Punctuation, Push("block")},
119			{caddyfileCommentRegexp, CommentSingle, Pop(2)},
120			{`\\\n`, Text, nil}, // Skip escaped newlines
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)},
129			Include("base"),
130		},
131		"deep_subdirective": {
132			{`\{(?=\s)`, Punctuation, Push("block")},
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)},
143			Include("base"),
144		},
145		"double_quotes": {
146			Include("placeholder"),
147			{`\\"`, StringDouble, nil},
148			{`[^"]`, StringDouble, nil},
149			{`"`, StringDouble, Pop(1)},
150		},
151		"backticks": {
152			Include("placeholder"),
153			{"\\\\`", StringBacktick, nil},
154			{"[^`]", StringBacktick, nil},
155			{"`", StringBacktick, Pop(1)},
156		},
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)},
164		},
165		"heredoc": {
166			{`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
167		},
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},
176		},
177		"base": {
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},
190			{`/[^\s#]*`, Name, nil},
191			{`\s+`, Text, nil},
192		},
193	}
194}
195
196// Caddyfile lexer.
197var Caddyfile = Register(MustNewLexer(
198	&Config{
199		Name:      "Caddyfile",
200		Aliases:   []string{"caddyfile", "caddy"},
201		Filenames: []string{"Caddyfile*"},
202		MimeTypes: []string{},
203	},
204	caddyfileRules,
205))
206
207func caddyfileRules() Rules {
208	return Rules{
209		"root": {
210			{caddyfileCommentRegexp, CommentSingle, nil},
211			// Global options block
212			{`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
213			// Top level import
214			{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
215			// Snippets
216			{`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
217			// Site label
218			{`[^#{(\s,]+`, GenericHeading, Push("label")},
219			// Site label with placeholder
220			{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
221			{`\s+`, Text, nil},
222		},
223		"globals": {
224			{`\}`, Punctuation, Pop(1)},
225			// Global options are parsed as subdirectives (no matcher)
226			{`[^\s#]+`, Keyword, Push("subdirective")},
227			Include("base"),
228		},
229		"snippet": {
230			{`\}`, Punctuation, Pop(1)},
231			Include("site_body"),
232			// Any other directive
233			{`[^\s#]+`, Keyword, Push("directive")},
234			Include("base"),
235		},
236		"label": {
237			// Allow multiple labels, comma separated, newlines after
238			// a comma means another label is coming
239			{`,\s*\n?`, Text, nil},
240			{` `, Text, nil},
241			// Site label with placeholder
242			Include("placeholder"),
243			// Site label
244			{`[^#{(\s,]+`, GenericHeading, nil},
245			// Comment after non-block label (hack because comments end in \n)
246			{`#.*\n`, CommentSingle, Push("site_block")},
247			// Note: if \n, we'll never pop out of the site_block, it's valid
248			{`\{(?=\s)|\n`, Punctuation, Push("site_block")},
249		},
250		"site_block": {
251			{`\}`, Punctuation, Pop(2)},
252			Include("site_block_common"),
253		},
254	}.Merge(caddyfileCommonRules())
255}
256
257// Caddyfile directive-only lexer.
258var CaddyfileDirectives = Register(MustNewLexer(
259	&Config{
260		Name:      "Caddyfile Directives",
261		Aliases:   []string{"caddyfile-directives", "caddyfile-d", "caddy-d"},
262		Filenames: []string{},
263		MimeTypes: []string{},
264	},
265	caddyfileDirectivesRules,
266))
267
268func caddyfileDirectivesRules() Rules {
269	return Rules{
270		// Same as "site_block" in Caddyfile
271		"root": {
272			Include("site_block_common"),
273		},
274	}.Merge(caddyfileCommonRules())
275}