diff --git a/lexers/c/caddyfile.go b/lexers/c/caddyfile.go new file mode 100644 index 000000000..e8e6e9f3a --- /dev/null +++ b/lexers/c/caddyfile.go @@ -0,0 +1,205 @@ +package c + +import ( + . "github.com/alecthomas/chroma" // nolint + "github.com/alecthomas/chroma/lexers/internal" +) + +// CaddyfileCommon are the rules common to both of the lexer variants +var CaddyfileCommon = Rules{ + "site_block_common": { + // Import keyword + {`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil}, + // Matcher definition + {`@[^\s]+\s+`, NameDecorator, Push("matcher")}, + // These are special, they can nest more directives + {`handle|route|handle_path|not`, Keyword, Push("nested_directive")}, + // Any other directive + {`[^\s#]+`, Keyword, Push("directive")}, + Include("base"), + }, + "matcher": { + {`\{`, Punctuation, Push("block")}, + // Not can be one-liner + {`not`, Keyword, Push("deep_not_matcher")}, + // Any other same-line matcher + {`[^\s#]+`, Keyword, Push("arguments")}, + // Terminators + {`\n`, Text, Pop(1)}, + {`\}`, Punctuation, Pop(1)}, + Include("base"), + }, + "block": { + {`\}`, Punctuation, Pop(2)}, + // Not can be one-liner + {`not`, Keyword, Push("not_matcher")}, + // Any other subdirective + {`[^\s#]+`, Keyword, Push("subdirective")}, + Include("base"), + }, + "nested_block": { + {`\}`, Punctuation, Pop(2)}, + // Matcher definition + {`@[^\s]+\s+`, NameDecorator, Push("matcher")}, + // Any other directive + {`[^\s#]+`, Keyword, Push("nested_directive")}, + Include("base"), + }, + "not_matcher": { + {`\}`, Punctuation, Pop(2)}, + {`\{(?=\s)`, Punctuation, Push("block")}, + {`[^\s#]+`, Keyword, Push("arguments")}, + {`\s+`, Text, nil}, + }, + "deep_not_matcher": { + {`\}`, Punctuation, Pop(2)}, + {`\{(?=\s)`, Punctuation, Push("block")}, + {`[^\s#]+`, Keyword, Push("deep_subdirective")}, + {`\s+`, Text, nil}, + }, + "directive": { + {`\{(?=\s)`, Punctuation, Push("block")}, + Include("matcher_token"), + Include("comments_pop"), + {`\n`, Text, Pop(1)}, + Include("base"), + }, + "nested_directive": { + {`\{(?=\s)`, Punctuation, Push("nested_block")}, + Include("matcher_token"), + Include("comments_pop"), + {`\n`, Text, Pop(1)}, + Include("base"), + }, + "subdirective": { + {`\{(?=\s)`, Punctuation, Push("block")}, + Include("comments_pop"), + {`\n`, Text, Pop(1)}, + Include("base"), + }, + "arguments": { + {`\{(?=\s)`, Punctuation, Push("block")}, + Include("comments_pop"), + {`\n`, Text, Pop(2)}, + Include("base"), + }, + "deep_subdirective": { + {`\{(?=\s)`, Punctuation, Push("block")}, + Include("comments_pop"), + {`\n`, Text, Pop(3)}, + Include("base"), + }, + "matcher_token": { + {`@[^\s]+`, NameDecorator, Push("arguments")}, // Named matcher + {`/[^\s]+`, NameDecorator, Push("arguments")}, // Path matcher + {`\*`, NameDecorator, Push("arguments")}, // Wildcard path matcher + {`\[\\]`, NameDecorator, Push("arguments")}, // Matcher token stub for docs + }, + "comments": { + {`^#.*\n`, CommentSingle, nil}, // Comment at start of line + {`\s+#.*\n`, CommentSingle, nil}, // Comment preceded by whitespace + }, + "comments_pop": { + {`^#.*\n`, CommentSingle, Pop(1)}, // Comment at start of line + {`\s+#.*\n`, CommentSingle, Pop(1)}, // Comment preceded by whitespace + }, + "base": { + Include("comments"), + {`on|off`, NameConstant, nil}, + {`(https?://)?([a-z0-9.-]+)(:)([0-9]+)`, ByGroups(Name, Name, Punctuation, LiteralNumberInteger), nil}, + {`[a-z-]+/[a-z-+]+`, LiteralString, nil}, + {`[0-9]+[km]?\b`, LiteralNumberInteger, nil}, + {`\{[\w+.-]+\}`, NameAttribute, nil}, // Placeholder + {`[^\s#{}$]+`, LiteralString, nil}, + {`/[^\s#]*`, Name, nil}, + {`\s+`, Text, nil}, + }, +} + +// CaddyfileRules are the merged rules for the main Caddyfile lexer +var CaddyfileRules = (func(a Rules, b Rules) Rules { + for k, v := range b { + a[k] = v + } + return a +})( + Rules{ + "root": { + Include("comments"), + // Global options block + {`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")}, + // Snippets + {`(\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")}, + // Site label + {`[^#{(\s,]+`, NameLabel, Push("label")}, + // Site label with placeholder + {`\{[\w+.-]+\}`, NameAttribute, Push("label")}, + {`\s+`, Text, nil}, + }, + "globals": { + {`\}`, Punctuation, Pop(1)}, + {`[^\s#]+`, KeywordType, Push("directive")}, + Include("base"), + }, + "snippet": { + {`\}`, Punctuation, Pop(1)}, + // Matcher definition + {`@[^\s]+\s+`, NameDecorator, Push("matcher")}, + // Any directive + {`[^\s#]+`, KeywordType, Push("directive")}, + Include("base"), + }, + "label": { + {`,`, Text, nil}, + {` `, Text, nil}, + {`[^#{(\s,]+`, NameLabel, nil}, + // Comment after non-block label (hack because comments end in \n) + {`#.*\n`, CommentSingle, Push("site_block")}, + // Note: if \n, we'll never pop out of the site_block, it's valid + {`\{(?=\s)|\n`, Punctuation, Push("site_block")}, + }, + "site_block": { + {`\}`, Punctuation, Pop(2)}, + Include("site_block_common"), + }, + }, + CaddyfileCommon, +) + +// CaddyfileDirectiveRules are the merged rules for the secondary lexer +var CaddyfileDirectiveRules = (func(a Rules, b Rules) Rules { + for k, v := range b { + a[k] = v + } + return a +})( + Rules{ + // Same as "site_block" in Caddyfile + "root": { + Include("site_block_common"), + }, + }, + CaddyfileCommon, +) + +// Caddyfile lexer. +var Caddyfile = internal.Register(MustNewLexer( + &Config{ + Name: "Caddyfile", + Aliases: []string{"caddyfile", "caddy"}, + Filenames: []string{"Caddyfile*"}, + MimeTypes: []string{}, + }, + CaddyfileRules, +)) + +// Caddyfile directive-only lexer. +var CaddyfileDirectives = internal.Register(MustNewLexer( + &Config{ + Name: "Caddyfile Directives", + Aliases: []string{"caddyfile-directives", "caddyfile-d", "caddy-d"}, + Filenames: []string{}, + MimeTypes: []string{}, + }, + CaddyfileDirectiveRules, +)) diff --git a/lexers/testdata/caddyfile.actual b/lexers/testdata/caddyfile.actual new file mode 100644 index 000000000..9f6767efb --- /dev/null +++ b/lexers/testdata/caddyfile.actual @@ -0,0 +1,70 @@ +{ + debug + admin off + on_demand_tls { + ask https://example.com + } +} + +(blocking) { + @blocked { + path *.txt *.md *.mdown /site/* + } + redir @blocked / +} + +example.com, fake.org { + root * /srv + + tls off + + route { + # Add trailing slash for directory requests + @canonicalPath { + file { + try_files {path}/index.php + } + not path */ + } + redir @canonicalPath {path}/ 308 + + # If the requested file does not exist, try index files + @indexFiles { + file { + try_files {path} {path}/index.php index.php + split_path .php + } + } + rewrite @indexFiles {http.matchers.file.relative} + + # Proxy PHP files to the FastCGI responder + @phpFiles { + path *.php + } + reverse_proxy @phpFiles unix//var/run/php7.4-fpm.sock { + transport fastcgi { + split .php + } + } + } + + @encode_exts { + path / *.html *.js *.css *.svg + } + + header { + X-Content-Type-Options nosniff + X-XSS-Protection "1; mode=block" + X-Robots-Tag none + Content-Security-Policy "frame-ancestors 'self'" + X-Frame-Options DENY + Referrer-Policy same-origin + } + + @singleLine not path /matcher + respond @singleLine "Awesome." + + import blocking + + file_server +} \ No newline at end of file diff --git a/lexers/testdata/caddyfile.expected b/lexers/testdata/caddyfile.expected new file mode 100644 index 000000000..989fd9481 --- /dev/null +++ b/lexers/testdata/caddyfile.expected @@ -0,0 +1,239 @@ +[ + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t"}, + {"type":"KeywordType","value":"debug"}, + {"type":"Text","value":"\n\t"}, + {"type":"KeywordType","value":"admin"}, + {"type":"Text","value":" "}, + {"type":"NameConstant","value":"off"}, + {"type":"Text","value":"\n\t"}, + {"type":"KeywordType","value":"on_demand_tls"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"ask"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"https://example.com"}, + {"type":"Text","value":"\n\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\n"}, + {"type":"NameVariableAnonymous","value":"(blocking)"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":" \n\t"}, + {"type":"NameDecorator","value":"@blocked "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.txt"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.md"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.mdown"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"/site/*"}, + {"type":"Text","value":"\n\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t"}, + {"type":"KeywordType","value":"redir"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"@blocked"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"/"}, + {"type":"Text","value":"\n"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\n"}, + {"type":"NameLabel","value":"example.com"}, + {"type":"Text","value":", "}, + {"type":"NameLabel","value":"fake.org"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t"}, + {"type":"Keyword","value":"root"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"*"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"/srv"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"Keyword","value":"tls"}, + {"type":"Text","value":" "}, + {"type":"NameConstant","value":"off"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"Keyword","value":"route"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"CommentSingle","value":"\n\t\t# Add trailing slash for directory requests\n"}, + {"type":"Text","value":"\t\t"}, + {"type":"NameDecorator","value":"@canonicalPath "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Keyword","value":"file"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t\t"}, + {"type":"Keyword","value":"try_files"}, + {"type":"Text","value":" "}, + {"type":"NameAttribute","value":"{path}"}, + {"type":"LiteralString","value":"/index.php"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Keyword","value":"not"}, + {"type":"Text","value":" "}, + {"type":"Keyword","value":"path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*/"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"redir"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"@canonicalPath"}, + {"type":"Text","value":" "}, + {"type":"NameAttribute","value":"{path}"}, + {"type":"LiteralString","value":"/"}, + {"type":"Text","value":" "}, + {"type":"LiteralNumberInteger","value":"308"}, + {"type":"CommentSingle","value":"\n\n\t\t# If the requested file does not exist, try index files\n"}, + {"type":"Text","value":"\t\t"}, + {"type":"NameDecorator","value":"@indexFiles"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Keyword","value":"file"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t\t"}, + {"type":"Keyword","value":"try_files"}, + {"type":"Text","value":" "}, + {"type":"NameAttribute","value":"{path}"}, + {"type":"Text","value":" "}, + {"type":"NameAttribute","value":"{path}"}, + {"type":"LiteralString","value":"/index.php"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"index.php"}, + {"type":"Text","value":"\n\t\t\t\t"}, + {"type":"Keyword","value":"split_path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":".php"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"rewrite"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"@indexFiles"}, + {"type":"Text","value":" "}, + {"type":"NameAttribute","value":"{http.matchers.file.relative}"}, + {"type":"CommentSingle","value":"\n\n\t\t# Proxy PHP files to the FastCGI responder\n"}, + {"type":"Text","value":"\t\t"}, + {"type":"NameDecorator","value":"@phpFiles"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Keyword","value":"path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.php"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"reverse_proxy"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"@phpFiles"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"unix//var/run/php7.4-fpm.sock"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Keyword","value":"transport"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"fastcgi"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t\t\t"}, + {"type":"Keyword","value":"split"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":".php"}, + {"type":"Text","value":"\n\t\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"NameDecorator","value":"@encode_exts "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"/"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.html"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.js"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.css"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"*.svg"}, + {"type":"Text","value":"\n\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"Keyword","value":"header"}, + {"type":"Text","value":" "}, + {"type":"Punctuation","value":"{"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"X-Content-Type-Options"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"nosniff"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"X-XSS-Protection"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"\"1;"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"mode=block\""}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"X-Robots-Tag"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"none"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"Content-Security-Policy"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"\"frame-ancestors"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"'self'\""}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"X-Frame-Options"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"DENY"}, + {"type":"Text","value":"\n\t\t"}, + {"type":"Keyword","value":"Referrer-Policy"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"same-origin"}, + {"type":"Text","value":"\n\t"}, + {"type":"Punctuation","value":"}"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"NameDecorator","value":"@singleLine "}, + {"type":"Keyword","value":"not"}, + {"type":"Text","value":" "}, + {"type":"Keyword","value":"path"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"/matcher"}, + {"type":"Text","value":"\n\t"}, + {"type":"Keyword","value":"respond"}, + {"type":"Text","value":" "}, + {"type":"NameDecorator","value":"@singleLine"}, + {"type":"Text","value":" "}, + {"type":"LiteralString","value":"\"Awesome.\""}, + {"type":"Text","value":"\n\n\t"}, + {"type":"Keyword","value":"import"}, + {"type":"Text","value":" "}, + {"type":"NameVariableMagic","value":"blocking"}, + {"type":"Text","value":"\n\n\t"}, + {"type":"Keyword","value":"file_server"}, + {"type":"Text","value":"\n"}, + {"type":"Punctuation","value":"}"} +]