Skip to content

Commit 517ac61

Browse files
committed
Unfinished parser
1 parent a6347c2 commit 517ac61

File tree

8 files changed

+1432
-1
lines changed

8 files changed

+1432
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"php": ">=8.2"
1414
},
1515
"require-dev": {
16+
"ircmaxell/php-yacc": "dev-master",
1617
"jbboehr/handlebars-spec": "dev-master",
1718
"phpstan/phpstan": "^2.1",
1819
"phpunit/phpunit": "^11.5"

composer.lock

Lines changed: 54 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

grammar/handlebars.y

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
%token BOOLEAN
2+
%token CLOSE
3+
%token CLOSE_ARRAY
4+
%token CLOSE_BLOCK_PARAMS
5+
%token CLOSE_RAW_BLOCK
6+
%token CLOSE_SEXPR
7+
%token CLOSE_UNESCAPED
8+
%token COMMENT
9+
%token CONTENT
10+
%token DATA
11+
%token END_RAW_BLOCK
12+
%token EQUALS
13+
%token ID
14+
%token INVERSE
15+
%token NULL
16+
%token NUMBER
17+
%token OPEN
18+
%token OPEN_ARRAY
19+
%token OPEN_BLOCK
20+
%token OPEN_BLOCK_PARAMS
21+
%token OPEN_ENDBLOCK
22+
%token OPEN_INVERSE
23+
%token OPEN_INVERSE_CHAIN
24+
%token OPEN_PARTIAL
25+
%token OPEN_PARTIAL_BLOCK
26+
%token OPEN_RAW_BLOCK
27+
%token OPEN_SEXPR
28+
%token OPEN_UNESCAPED
29+
%token PRIVATE_SEP
30+
%token SEP
31+
%token STRING
32+
%token UNDEFINED
33+
34+
%%
35+
36+
/*
37+
* Should match the grammar (as of 2025-01-15) of
38+
* https://github.com/handlebars-lang/handlebars-parser/blob/master/src/handlebars.yy
39+
* EBNF grammar has been converted to BNF.
40+
*/
41+
42+
program:
43+
statement_list { $$ = $self->prepareProgram($self->semStack[$1]); }
44+
;
45+
46+
statement_list:
47+
statement_list statement { if ($self->semStack[$2] !== null) { $self->semStack[$1][] = $self->semStack[$2]; } $$ = $self->semStack[$1]; }
48+
| /* empty */ { $$ = []; }
49+
50+
statement:
51+
mustache { $$ = $self->semStack[$1]; }
52+
| block { $$ = $self->semStack[$1]; }
53+
| rawBlock { $$ = $self->semStack[$1]; }
54+
| partial { $$ = $self->semStack[$1]; }
55+
| partialBlock { $$ = $self->semStack[$1]; }
56+
| content { $$ = $self->semStack[$1]; }
57+
| COMMENT {
58+
$$ = [
59+
'type' => 'CommentStatement',
60+
'value' => $self->stripComment($1),
61+
'strip' => $self->stripFlags($1, $1),
62+
];
63+
};
64+
65+
content:
66+
CONTENT {
67+
$$ = [
68+
'type' => 'ContentStatement',
69+
'value' => $self->semStack[$1],
70+
];
71+
}
72+
;
73+
74+
content_list:
75+
content_list content { if ($self->semStack[$2] !== null) { $self->semStack[$1][] = $self->semStack[$2]; } $$ = $self->semStack[$1]; }
76+
| /* empty */ { $$ = []; }
77+
;
78+
79+
rawBlock:
80+
openRawBlock content_list END_RAW_BLOCK { $self->prepareRawBlock($self->semStack[$1], $self->semStack[$2], $self->semStack[$3]); }
81+
;
82+
83+
openRawBlock:
84+
OPEN_RAW_BLOCK helperName expr_list optional_hash CLOSE_RAW_BLOCK { $$ = ['path' => $self->semStack[$2], 'params' => $self->semStack[$3], 'hash' => $self->semStack[$4]]; }
85+
;
86+
87+
block:
88+
openBlock program optional_inverseChain closeBlock { $$ = $self->prepareBlock($1, $2, $3, $4, false); }
89+
| openInverse program optional_inverseAndProgram closeBlock { $$ = $self->prepareBlock($1, $2, $3, $4, true); }
90+
;
91+
92+
openBlock:
93+
OPEN_BLOCK helperName expr_list optional_hash optional_blockParams CLOSE {
94+
$$ = [
95+
'open' => $self->semStack[$1],
96+
'path' => $self->semStack[$2],
97+
'params' => $self->semStack[$3],
98+
'hash' => $self->semStack[$4],
99+
'blockParams' => $self->semStack[$5],
100+
'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$6])
101+
];
102+
}
103+
;
104+
105+
openInverse:
106+
OPEN_INVERSE helperName expr_list optional_hash optional_blockParams CLOSE {
107+
$$ = [
108+
'path' => $self->semStack[$2],
109+
'params' => $self->semStack[$3],
110+
'hash' => $self->semStack[$4],
111+
'blockParams' => $self->semStack[$5],
112+
'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$6])
113+
];
114+
}
115+
;
116+
117+
openInverseChain:
118+
OPEN_INVERSE_CHAIN helperName expr_list optional_hash optional_blockParams CLOSE {
119+
$$ = [
120+
'path' => $self->semStack[$2],
121+
'params' => $self->semStack[$3],
122+
'hash' => $self->semStack[$4],
123+
'blockParams' => $self->semStack[$5],
124+
'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$6])
125+
];
126+
}
127+
;
128+
129+
optional_inverseAndProgram:
130+
inverseAndProgram
131+
| /* empty */ { $$ = null; }
132+
;
133+
134+
inverseAndProgram:
135+
INVERSE program { $$ = ['strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$1]), 'program' => $self->semStack[$2]]; }
136+
;
137+
138+
optional_inverseChain:
139+
inverseChain
140+
| /* empty */ { $$ = null; }
141+
;
142+
143+
inverseChain:
144+
openInverseChain program optional_inverseChain {
145+
$inverse = $self->prepareBlock($self->semStack[$1], $self->semStack[$2], $self->semStack[$3], $self->semStack[$3], false);
146+
$program = $self->prepareProgram([$inverse], $self->semStack[$2]['loc']);
147+
$program->chained = true;
148+
149+
$$ = ['strip' => $self->semStack[$1]['strip'], 'program' => $program, 'chain' => true];
150+
}
151+
| inverseAndProgram { $$ = $self->semStack[$1]; }
152+
;
153+
154+
closeBlock:
155+
OPEN_ENDBLOCK helperName CLOSE { $$ = ['path' => $self->semStack[$2], 'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$3])]; }
156+
;
157+
158+
mustache:
159+
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
160+
// This also allows for handler unification as all mustache node instances can utilize the same handler
161+
OPEN hash CLOSE { $$ = $self->prepareMustache($self->syntax->hash($self->semStack[$2], ['syntax' => 'expr']), [], null, $self->semStack[$1], $self->stripFlags($self->semStack[$1], $self->semStack[$3])); }
162+
| OPEN expr expr_list optional_hash CLOSE { $$ = $self->prepareMustache($self->semStack[$2], $self->semStack[$3], $self->semStack[$4], $self->semStack[$1], $self->stripFlags($self->semStack[$1], $self->semStack[$5])); }
163+
| OPEN_UNESCAPED expr expr_list optional_hash CLOSE_UNESCAPED { $$ = $self->prepareMustache($self->semStack[$2], $self->semStack[$3], $self->semStack[$4], $self->semStack[$1], $self->stripFlags($self->semStack[$1], $self->semStack[$5])); }
164+
;
165+
166+
partial:
167+
OPEN_PARTIAL expr expr_list optional_hash CLOSE {
168+
$$ = [
169+
'type' => 'PartialStatement',
170+
'name' => $self->semStack[$2],
171+
'params' => $self->semStack[$3],
172+
'hash' => $self->semStack[$4],
173+
'indent' => '',
174+
'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$5]),
175+
];
176+
}
177+
;
178+
179+
partialBlock:
180+
openPartialBlock program closeBlock { $$ = $self->preparePartialBlock($self->semStack[$1], $self->semStack[$2], $self->semStack[$3]); }
181+
;
182+
183+
openPartialBlock:
184+
OPEN_PARTIAL_BLOCK expr expr_list optional_hash CLOSE {
185+
$$ = [
186+
'path' => $self->semStack[$2],
187+
'params' => $self->semStack[$3],
188+
'hash' => $self->semStack[$4],
189+
'strip' => $self->stripFlags($self->semStack[$1], $self->semStack[$5]),
190+
];
191+
}
192+
;
193+
194+
expr_list:
195+
expr_list expr { if ($self->semStack[$2] !== null) { $self->semStack[$1][] = $self->semStack[$2]; } $$ = $self->semStack[$1]; }
196+
| /* empty */ { $$ = []; }
197+
;
198+
199+
expr:
200+
helperName { $$ = $self->semStack[$1]; }
201+
| exprHead { $$ = $self->semStack[$1]; }
202+
;
203+
204+
exprHead:
205+
arrayLiteral { $$ = $self->semStack[$1]; }
206+
| sexpr { $$ = $self->semStack[$1]; }
207+
;
208+
209+
sexpr:
210+
OPEN_SEXPR hash CLOSE_SEXPR { $$ = $self->syntax->hash($self->semStack[$2], ['syntax' => 'expr']); }
211+
| OPEN_SEXPR expr expr_list optional_hash CLOSE_SEXPR {
212+
$$ = [
213+
'type' => 'SubExpression',
214+
'path' => $self->semStack[$2],
215+
'params' => $self->semStack[$3],
216+
'hash' => $self->semStack[$4],
217+
];
218+
}
219+
;
220+
221+
hash:
222+
non_empty_hashSegment_list { $$ = ['type' => 'Hash', 'pairs' => $self->semStack[$1]]; }
223+
;
224+
225+
optional_hash:
226+
hash
227+
| /* empty */ { $$ = []; }
228+
;
229+
230+
non_empty_hashSegment_list:
231+
hashSegment { $$ = [$self->semStack[$1]]; }
232+
| non_empty_hashSegment_list hashSegment {if ($self->semStack[$2] !== null) { $self->semStack[$1][] = $self->semStack[$2]; } $$ = $self->semStack[$1];}
233+
;
234+
235+
hashSegment:
236+
ID EQUALS expr { $$ = ['type' => 'HashPair', 'key' => $self->id($self->semStack[$1]), 'value' => $self->semStack[$3]]; }
237+
;
238+
239+
arrayLiteral:
240+
OPEN_ARRAY expr_list CLOSE_ARRAY { $$ = $self->syntax->square($self->semStack[$2], ['syntax' => 'expr']); }
241+
;
242+
243+
optional_blockParams:
244+
blockParams
245+
| /* empty */ { $$ = []; }
246+
;
247+
248+
non_empty_ID_list:
249+
ID { $$ = [$self->semStack[$1]]; }
250+
| non_empty_ID_list ID {if ($self->semStack[$2] !== null) { $self->semStack[$1][] = $self->semStack[$2]; } $$ = $self->semStack[$1];}
251+
;
252+
253+
blockParams:
254+
OPEN_BLOCK_PARAMS non_empty_ID_list CLOSE_BLOCK_PARAMS { $$ = $self->id($self->semStack[$2]); }
255+
;
256+
257+
helperName:
258+
path { $$ = $self->semStack[$1]; }
259+
| dataName { $$ = $self->semStack[$1]; }
260+
| STRING { $$ = ['type' => 'StringLiteral', 'value' => $self->semStack[$1]]; }
261+
| NUMBER { $$ = ['type' => 'NumberLiteral', 'value' => $self->semStack[$1] + 0]; }
262+
| BOOLEAN { $$ = ['type' => 'BooleanLiteral', 'value' => $self->semStack[$1] === 'true']; }
263+
| UNDEFINED { $$ = ['type' => 'UndefinedLiteral', 'value' => null]; }
264+
| NULL { $$ = ['type' => 'NullLiteral', 'value' => null]; }
265+
;
266+
267+
dataName:
268+
DATA pathSegments {$$ = $self->preparePath(true, false, $self->semStack[$2]);}
269+
;
270+
271+
sep:
272+
SEP { $$ = $self->semStack[$1]; }
273+
| PRIVATE_SEP { $$ = $self->semStack[$1]; }
274+
;
275+
276+
path:
277+
exprHead sep pathSegments {$$ = $self->preparePath(false, $self->semStack[$1], $self->semStack[$3]);}
278+
| pathSegments {$$ = $self->preparePath(false, false, $self->semStack[$1]);}
279+
;
280+
281+
pathSegments:
282+
pathSegments sep ID {
283+
$self->semStack[$1][] = ['part' => $self->id($self->semStack[$3]), 'original' => $self->semStack[$3], 'separator' => $self->semStack[$2]];
284+
$$ = $self->semStack[$1];
285+
}
286+
| ID {$$ = [['part' => $self->id($self->semStack[$1]), 'original' => $self->semStack[$1]]]; }
287+
;
288+
289+
%%

0 commit comments

Comments
 (0)