Skip to content

Commit deca444

Browse files
authored
feat(lexers): add Hurl lexer (#1298)
## Hurl lexer Adds a lexer for [Hurl](https://hurl.dev) — the plain-text HTTP/API testing format (`*.hurl`). Today these files fall back to plaintext, but there's a lot worth colouring. Handles: - request methods (`GET`, `POST`, `DELETE`, ...) and the URL that follows - the `HTTP <status>` response line - `[Sections]` — `[Asserts]`, `[Captures]`, `[Options]`, `[QueryStringParams]`, `[FormParams]`, `[MultipartFormData]`, `[Cookies]`, `[BasicAuth]` - the query / predicate / filter DSL used in asserts & captures (`jsonpath`, `header`, `contains`, `matches`, `isIsoDate`, `count`, `split`, ...) - `{{templates}}` (in URLs, strings and bodies) - `"quoted"` and ` ```triple-quoted``` ` bodies, numbers, booleans and comments Includes `testdata/hurl.actual` + `hurl.expected`; `go test ./...` passes, and `Hurl` is added to the README language list. Co-authored-by: cc1a2b <cc1a2b@users.noreply.github.com>
1 parent c74f282 commit deca444

4 files changed

Lines changed: 372 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ translators for Pygments lexers and styles.
4646
| E | EBNF, Elixir, Elm, EmacsLisp, ERB, Erlang
4747
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp
4848
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gettext, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy
49-
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy
49+
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hurl, Hy
5050
| I | Idris, Igor, INI, Io, ISCdhcpd
5151
| J | J, Janet, Java, JavaScript, JSON, JSONata, JQ, Jsonnet, Julia, Jungle
5252
| K | Kakoune, KDL, Kotlin

lexers/embedded/hurl.xml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<lexer>
2+
<config>
3+
<name>Hurl</name>
4+
<alias>hurl</alias>
5+
<filename>*.hurl</filename>
6+
<mime_type>text/x-hurl</mime_type>
7+
</config>
8+
<rules>
9+
<state name="root">
10+
<rule pattern="\s+">
11+
<token type="TextWhitespace"/>
12+
</rule>
13+
<rule pattern="#.*">
14+
<token type="CommentSingle"/>
15+
</rule>
16+
<rule pattern="(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH|LINK|UNLINK|PURGE|LOCK|UNLOCK|PROPFIND|PROPPATCH|MKCOL|COPY|MOVE|REPORT|SEARCH|VIEW)([ \t]+)">
17+
<bygroups>
18+
<token type="Keyword"/>
19+
<token type="TextWhitespace"/>
20+
</bygroups>
21+
<push state="url"/>
22+
</rule>
23+
<rule pattern="HTTP(/\d(\.\d)?)?\b">
24+
<token type="KeywordReserved"/>
25+
</rule>
26+
<rule pattern="\[[A-Z][A-Za-z0-9]*\]">
27+
<token type="NameDecorator"/>
28+
</rule>
29+
<rule pattern="\{\{">
30+
<token type="NameVariable"/>
31+
<push state="template"/>
32+
</rule>
33+
<rule pattern="```">
34+
<token type="LiteralString"/>
35+
<push state="multiline"/>
36+
</rule>
37+
<rule pattern="&#34;">
38+
<token type="LiteralStringDouble"/>
39+
<push state="string"/>
40+
</rule>
41+
<rule pattern="\b(true|false|null)\b">
42+
<token type="KeywordConstant"/>
43+
</rule>
44+
<rule pattern="\b(status|url|header|certificate|cookie|body|bytes|xpath|jsonpath|regex|variable|duration|sha256|md5|ip|redirects|version)\b">
45+
<token type="NameBuiltin"/>
46+
</rule>
47+
<rule pattern="\b(count|daysAfterNow|daysBeforeNow|decode|format|htmlEscape|htmlUnescape|nth|replace|replaceRegex|split|toDate|toFloat|toInt|toString|urlDecode|urlEncode|urlQueryParam|base64Decode|base64Encode|base64UrlSafeDecode|base64UrlSafeEncode|first|last|location)\b">
48+
<token type="NameFunction"/>
49+
</rule>
50+
<rule pattern="\b(not|contains|exists|includes|matches|startsWith|endsWith|isBoolean|isCollection|isDate|isEmpty|isFloat|isInteger|isIsoDate|isNumber|isString|isIpv4|isIpv6|isUuid)\b">
51+
<token type="OperatorWord"/>
52+
</rule>
53+
<rule pattern="==|!=|&gt;=|&lt;=|&gt;|&lt;">
54+
<token type="Operator"/>
55+
</rule>
56+
<rule pattern="[+-]?\d+(\.\d+)?\b">
57+
<token type="LiteralNumber"/>
58+
</rule>
59+
<rule pattern="[A-Za-z_][A-Za-z0-9_-]*">
60+
<token type="Name"/>
61+
</rule>
62+
<rule pattern="[:{}\[\],*]">
63+
<token type="Punctuation"/>
64+
</rule>
65+
<rule pattern=".">
66+
<token type="Text"/>
67+
</rule>
68+
</state>
69+
<state name="url">
70+
<rule pattern="\n">
71+
<token type="TextWhitespace"/>
72+
<pop depth="1"/>
73+
</rule>
74+
<rule pattern="\{\{">
75+
<token type="NameVariable"/>
76+
<push state="template"/>
77+
</rule>
78+
<rule pattern="[^\s{]+">
79+
<token type="NameOther"/>
80+
</rule>
81+
<rule pattern="[ \t]+">
82+
<token type="TextWhitespace"/>
83+
</rule>
84+
</state>
85+
<state name="template">
86+
<rule pattern="\}\}">
87+
<token type="NameVariable"/>
88+
<pop depth="1"/>
89+
</rule>
90+
<rule pattern="[^}]+">
91+
<token type="NameVariable"/>
92+
</rule>
93+
<rule pattern="\}">
94+
<token type="NameVariable"/>
95+
</rule>
96+
</state>
97+
<state name="string">
98+
<rule pattern="\\.">
99+
<token type="LiteralStringEscape"/>
100+
</rule>
101+
<rule pattern="\{\{">
102+
<token type="NameVariable"/>
103+
<push state="template"/>
104+
</rule>
105+
<rule pattern="&#34;">
106+
<token type="LiteralStringDouble"/>
107+
<pop depth="1"/>
108+
</rule>
109+
<rule pattern="[^&#34;\\{]+">
110+
<token type="LiteralStringDouble"/>
111+
</rule>
112+
<rule pattern=".">
113+
<token type="LiteralStringDouble"/>
114+
</rule>
115+
</state>
116+
<state name="multiline">
117+
<rule pattern="```">
118+
<token type="LiteralString"/>
119+
<pop depth="1"/>
120+
</rule>
121+
<rule pattern="\{\{">
122+
<token type="NameVariable"/>
123+
<push state="template"/>
124+
</rule>
125+
<rule pattern="[^`{]+">
126+
<token type="LiteralString"/>
127+
</rule>
128+
<rule pattern="[`{]">
129+
<token type="LiteralString"/>
130+
</rule>
131+
</state>
132+
</rules>
133+
</lexer>

lexers/testdata/hurl.actual

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Fetch the home page and check the title
2+
GET https://example.org/api/{{version}}/home
3+
User-Agent: hurl/6.0
4+
Accept: application/json
5+
[QueryStringParams]
6+
page: 1
7+
limit: 50
8+
9+
HTTP 200
10+
[Asserts]
11+
status == 200
12+
header "Content-Type" contains "application/json"
13+
jsonpath "$.items" count == 3
14+
jsonpath "$.enabled" == true
15+
jsonpath "$.token" matches /[a-f0-9]{32}/
16+
duration < 1000
17+
18+
# Create a resource, capture the id
19+
POST https://example.org/api/tests
20+
Authorization: Bearer {{token}}
21+
{
22+
"name": "scan",
23+
"targets": ["10.0.0.1", "10.0.0.2"],
24+
"active": true,
25+
"retries": 3
26+
}
27+
28+
HTTP 201
29+
[Captures]
30+
test_id: jsonpath "$.id"
31+
etag: header "ETag"
32+
[Asserts]
33+
jsonpath "$.name" == "scan"
34+
jsonpath "$.created_at" isIsoDate
35+
bytes count > 0
36+
37+
DELETE https://example.org/api/tests/{{test_id}}
38+
HTTP *
39+
[Options]
40+
retry: 5

lexers/testdata/hurl.expected

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
[
2+
{"type":"CommentSingle","value":"# Fetch the home page and check the title"},
3+
{"type":"TextWhitespace","value":"\n"},
4+
{"type":"Keyword","value":"GET"},
5+
{"type":"TextWhitespace","value":" "},
6+
{"type":"NameOther","value":"https://example.org/api/"},
7+
{"type":"NameVariable","value":"{{version}}"},
8+
{"type":"NameOther","value":"/home"},
9+
{"type":"TextWhitespace","value":"\n"},
10+
{"type":"Name","value":"User-Agent"},
11+
{"type":"Punctuation","value":":"},
12+
{"type":"TextWhitespace","value":" "},
13+
{"type":"Name","value":"hurl"},
14+
{"type":"Text","value":"/"},
15+
{"type":"LiteralNumber","value":"6.0"},
16+
{"type":"TextWhitespace","value":"\n"},
17+
{"type":"Name","value":"Accept"},
18+
{"type":"Punctuation","value":":"},
19+
{"type":"TextWhitespace","value":" "},
20+
{"type":"Name","value":"application"},
21+
{"type":"Text","value":"/"},
22+
{"type":"Name","value":"json"},
23+
{"type":"TextWhitespace","value":"\n"},
24+
{"type":"NameDecorator","value":"[QueryStringParams]"},
25+
{"type":"TextWhitespace","value":"\n"},
26+
{"type":"Name","value":"page"},
27+
{"type":"Punctuation","value":":"},
28+
{"type":"TextWhitespace","value":" "},
29+
{"type":"LiteralNumber","value":"1"},
30+
{"type":"TextWhitespace","value":"\n"},
31+
{"type":"Name","value":"limit"},
32+
{"type":"Punctuation","value":":"},
33+
{"type":"TextWhitespace","value":" "},
34+
{"type":"LiteralNumber","value":"50"},
35+
{"type":"TextWhitespace","value":"\n\n"},
36+
{"type":"KeywordReserved","value":"HTTP"},
37+
{"type":"TextWhitespace","value":" "},
38+
{"type":"LiteralNumber","value":"200"},
39+
{"type":"TextWhitespace","value":"\n"},
40+
{"type":"NameDecorator","value":"[Asserts]"},
41+
{"type":"TextWhitespace","value":"\n"},
42+
{"type":"NameBuiltin","value":"status"},
43+
{"type":"TextWhitespace","value":" "},
44+
{"type":"Operator","value":"=="},
45+
{"type":"TextWhitespace","value":" "},
46+
{"type":"LiteralNumber","value":"200"},
47+
{"type":"TextWhitespace","value":"\n"},
48+
{"type":"NameBuiltin","value":"header"},
49+
{"type":"TextWhitespace","value":" "},
50+
{"type":"LiteralStringDouble","value":"\"Content-Type\""},
51+
{"type":"TextWhitespace","value":" "},
52+
{"type":"OperatorWord","value":"contains"},
53+
{"type":"TextWhitespace","value":" "},
54+
{"type":"LiteralStringDouble","value":"\"application/json\""},
55+
{"type":"TextWhitespace","value":"\n"},
56+
{"type":"NameBuiltin","value":"jsonpath"},
57+
{"type":"TextWhitespace","value":" "},
58+
{"type":"LiteralStringDouble","value":"\"$.items\""},
59+
{"type":"TextWhitespace","value":" "},
60+
{"type":"NameFunction","value":"count"},
61+
{"type":"TextWhitespace","value":" "},
62+
{"type":"Operator","value":"=="},
63+
{"type":"TextWhitespace","value":" "},
64+
{"type":"LiteralNumber","value":"3"},
65+
{"type":"TextWhitespace","value":"\n"},
66+
{"type":"NameBuiltin","value":"jsonpath"},
67+
{"type":"TextWhitespace","value":" "},
68+
{"type":"LiteralStringDouble","value":"\"$.enabled\""},
69+
{"type":"TextWhitespace","value":" "},
70+
{"type":"Operator","value":"=="},
71+
{"type":"TextWhitespace","value":" "},
72+
{"type":"KeywordConstant","value":"true"},
73+
{"type":"TextWhitespace","value":"\n"},
74+
{"type":"NameBuiltin","value":"jsonpath"},
75+
{"type":"TextWhitespace","value":" "},
76+
{"type":"LiteralStringDouble","value":"\"$.token\""},
77+
{"type":"TextWhitespace","value":" "},
78+
{"type":"OperatorWord","value":"matches"},
79+
{"type":"TextWhitespace","value":" "},
80+
{"type":"Text","value":"/"},
81+
{"type":"Punctuation","value":"["},
82+
{"type":"Name","value":"a-f0-9"},
83+
{"type":"Punctuation","value":"]{"},
84+
{"type":"LiteralNumber","value":"32"},
85+
{"type":"Punctuation","value":"}"},
86+
{"type":"Text","value":"/"},
87+
{"type":"TextWhitespace","value":"\n"},
88+
{"type":"NameBuiltin","value":"duration"},
89+
{"type":"TextWhitespace","value":" "},
90+
{"type":"Operator","value":"\u003c"},
91+
{"type":"TextWhitespace","value":" "},
92+
{"type":"LiteralNumber","value":"1000"},
93+
{"type":"TextWhitespace","value":"\n\n"},
94+
{"type":"CommentSingle","value":"# Create a resource, capture the id"},
95+
{"type":"TextWhitespace","value":"\n"},
96+
{"type":"Keyword","value":"POST"},
97+
{"type":"TextWhitespace","value":" "},
98+
{"type":"NameOther","value":"https://example.org/api/tests"},
99+
{"type":"TextWhitespace","value":"\n"},
100+
{"type":"Name","value":"Authorization"},
101+
{"type":"Punctuation","value":":"},
102+
{"type":"TextWhitespace","value":" "},
103+
{"type":"Name","value":"Bearer"},
104+
{"type":"TextWhitespace","value":" "},
105+
{"type":"NameVariable","value":"{{token}}"},
106+
{"type":"TextWhitespace","value":"\n"},
107+
{"type":"Punctuation","value":"{"},
108+
{"type":"TextWhitespace","value":"\n "},
109+
{"type":"LiteralStringDouble","value":"\"name\""},
110+
{"type":"Punctuation","value":":"},
111+
{"type":"TextWhitespace","value":" "},
112+
{"type":"LiteralStringDouble","value":"\"scan\""},
113+
{"type":"Punctuation","value":","},
114+
{"type":"TextWhitespace","value":"\n "},
115+
{"type":"LiteralStringDouble","value":"\"targets\""},
116+
{"type":"Punctuation","value":":"},
117+
{"type":"TextWhitespace","value":" "},
118+
{"type":"Punctuation","value":"["},
119+
{"type":"LiteralStringDouble","value":"\"10.0.0.1\""},
120+
{"type":"Punctuation","value":","},
121+
{"type":"TextWhitespace","value":" "},
122+
{"type":"LiteralStringDouble","value":"\"10.0.0.2\""},
123+
{"type":"Punctuation","value":"],"},
124+
{"type":"TextWhitespace","value":"\n "},
125+
{"type":"LiteralStringDouble","value":"\"active\""},
126+
{"type":"Punctuation","value":":"},
127+
{"type":"TextWhitespace","value":" "},
128+
{"type":"KeywordConstant","value":"true"},
129+
{"type":"Punctuation","value":","},
130+
{"type":"TextWhitespace","value":"\n "},
131+
{"type":"LiteralStringDouble","value":"\"retries\""},
132+
{"type":"Punctuation","value":":"},
133+
{"type":"TextWhitespace","value":" "},
134+
{"type":"LiteralNumber","value":"3"},
135+
{"type":"TextWhitespace","value":"\n"},
136+
{"type":"Punctuation","value":"}"},
137+
{"type":"TextWhitespace","value":"\n\n"},
138+
{"type":"KeywordReserved","value":"HTTP"},
139+
{"type":"TextWhitespace","value":" "},
140+
{"type":"LiteralNumber","value":"201"},
141+
{"type":"TextWhitespace","value":"\n"},
142+
{"type":"NameDecorator","value":"[Captures]"},
143+
{"type":"TextWhitespace","value":"\n"},
144+
{"type":"Name","value":"test_id"},
145+
{"type":"Punctuation","value":":"},
146+
{"type":"TextWhitespace","value":" "},
147+
{"type":"NameBuiltin","value":"jsonpath"},
148+
{"type":"TextWhitespace","value":" "},
149+
{"type":"LiteralStringDouble","value":"\"$.id\""},
150+
{"type":"TextWhitespace","value":"\n"},
151+
{"type":"Name","value":"etag"},
152+
{"type":"Punctuation","value":":"},
153+
{"type":"TextWhitespace","value":" "},
154+
{"type":"NameBuiltin","value":"header"},
155+
{"type":"TextWhitespace","value":" "},
156+
{"type":"LiteralStringDouble","value":"\"ETag\""},
157+
{"type":"TextWhitespace","value":"\n"},
158+
{"type":"NameDecorator","value":"[Asserts]"},
159+
{"type":"TextWhitespace","value":"\n"},
160+
{"type":"NameBuiltin","value":"jsonpath"},
161+
{"type":"TextWhitespace","value":" "},
162+
{"type":"LiteralStringDouble","value":"\"$.name\""},
163+
{"type":"TextWhitespace","value":" "},
164+
{"type":"Operator","value":"=="},
165+
{"type":"TextWhitespace","value":" "},
166+
{"type":"LiteralStringDouble","value":"\"scan\""},
167+
{"type":"TextWhitespace","value":"\n"},
168+
{"type":"NameBuiltin","value":"jsonpath"},
169+
{"type":"TextWhitespace","value":" "},
170+
{"type":"LiteralStringDouble","value":"\"$.created_at\""},
171+
{"type":"TextWhitespace","value":" "},
172+
{"type":"OperatorWord","value":"isIsoDate"},
173+
{"type":"TextWhitespace","value":"\n"},
174+
{"type":"NameBuiltin","value":"bytes"},
175+
{"type":"TextWhitespace","value":" "},
176+
{"type":"NameFunction","value":"count"},
177+
{"type":"TextWhitespace","value":" "},
178+
{"type":"Operator","value":"\u003e"},
179+
{"type":"TextWhitespace","value":" "},
180+
{"type":"LiteralNumber","value":"0"},
181+
{"type":"TextWhitespace","value":"\n\n"},
182+
{"type":"Keyword","value":"DELETE"},
183+
{"type":"TextWhitespace","value":" "},
184+
{"type":"NameOther","value":"https://example.org/api/tests/"},
185+
{"type":"NameVariable","value":"{{test_id}}"},
186+
{"type":"TextWhitespace","value":"\n"},
187+
{"type":"KeywordReserved","value":"HTTP"},
188+
{"type":"TextWhitespace","value":" "},
189+
{"type":"Punctuation","value":"*"},
190+
{"type":"TextWhitespace","value":"\n"},
191+
{"type":"NameDecorator","value":"[Options]"},
192+
{"type":"TextWhitespace","value":"\n"},
193+
{"type":"Name","value":"retry"},
194+
{"type":"Punctuation","value":":"},
195+
{"type":"TextWhitespace","value":" "},
196+
{"type":"LiteralNumber","value":"5"},
197+
{"type":"TextWhitespace","value":"\n"}
198+
]

0 commit comments

Comments
 (0)