1use crate::{
2 ast::FunctionArgument, derive_ASTNode, errors::parse_lexing_error, ASTNode, Expression,
3 ParseError, ParseOptions, ParseResult, Span, TSXToken, Token, TokenReader,
4};
5use tokenizer_lib::sized_tokens::{TokenEnd, TokenReaderWithTokenEnds, TokenStart};
6use visitable_derive::Visitable;
7
8#[apply(derive_ASTNode)]
9#[derive(Debug, Clone, PartialEq, Visitable, get_field_by_type::GetFieldByType)]
10#[get_field_by_type_target(Span)]
11pub enum JSXRoot {
12 Element(JSXElement),
13 Fragment(JSXFragment),
14}
15
16#[apply(derive_ASTNode)]
17#[derive(Debug, Clone, PartialEq, Visitable, get_field_by_type::GetFieldByType)]
18#[get_field_by_type_target(Span)]
19pub struct JSXElement {
20 pub tag_name: String,
22 pub attributes: Vec<JSXAttribute>,
23 pub children: JSXElementChildren,
24 pub position: Span,
25}
26
27#[derive(Debug, Clone, PartialEq, Visitable)]
28#[apply(derive_ASTNode)]
29pub enum JSXElementChildren {
30 Children(Vec<JSXNode>),
31 SelfClosing,
33}
34
35impl From<JSXElement> for JSXNode {
36 fn from(value: JSXElement) -> JSXNode {
37 JSXNode::Element(value)
38 }
39}
40
41impl ASTNode for JSXElement {
42 fn from_reader(
43 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
44 state: &mut crate::ParsingState,
45 options: &ParseOptions,
46 ) -> ParseResult<Self> {
47 let start_position = reader.expect_next(TSXToken::JSXOpeningTagStart)?;
48 Self::from_reader_sub_start(reader, state, options, start_position)
49 }
50
51 fn to_string_from_buffer<T: source_map::ToString>(
52 &self,
53 buf: &mut T,
54 options: &crate::ToStringOptions,
55 local: crate::LocalToStringInformation,
56 ) {
57 buf.push('<');
58 buf.push_str(&self.tag_name);
59 for attribute in &self.attributes {
60 buf.push(' ');
61 attribute.to_string_from_buffer(buf, options, local);
62 }
63
64 match self.children {
65 JSXElementChildren::Children(ref children) => {
66 buf.push('>');
67 jsx_children_to_string(children, buf, options, local);
68 buf.push_str("</");
69 buf.push_str(&self.tag_name);
70 buf.push('>');
71 }
72 JSXElementChildren::SelfClosing => {
73 buf.push_str(">");
74 }
75 }
76 }
77
78 fn get_position(&self) -> Span {
79 self.position
80 }
81}
82
83#[apply(derive_ASTNode)]
84#[derive(Debug, Clone, PartialEq, Visitable, get_field_by_type::GetFieldByType)]
85#[get_field_by_type_target(Span)]
86pub struct JSXFragment {
87 pub children: Vec<JSXNode>,
88 pub position: Span,
89}
90
91impl ASTNode for JSXFragment {
92 fn get_position(&self) -> Span {
93 self.position
94 }
95
96 fn from_reader(
97 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
98 state: &mut crate::ParsingState,
99 options: &ParseOptions,
100 ) -> ParseResult<Self> {
101 let start = reader.expect_next(TSXToken::JSXFragmentStart)?;
102 Self::from_reader_sub_start(reader, state, options, start)
103 }
104
105 fn to_string_from_buffer<T: source_map::ToString>(
106 &self,
107 buf: &mut T,
108 options: &crate::ToStringOptions,
109 local: crate::LocalToStringInformation,
110 ) {
111 buf.push_str("<>");
112 jsx_children_to_string(&self.children, buf, options, local);
113 buf.push_str("</>");
114 }
115}
116
117impl JSXFragment {
118 fn from_reader_sub_start(
119 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
120 state: &mut crate::ParsingState,
121 options: &ParseOptions,
122 start: TokenStart,
123 ) -> ParseResult<Self> {
124 let children = parse_jsx_children(reader, state, options)?;
125 let end = reader.expect_next_get_end(TSXToken::JSXFragmentEnd)?;
126 Ok(Self { children, position: start.union(end) })
127 }
128}
129
130impl ASTNode for JSXRoot {
131 fn from_reader(
132 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
133 state: &mut crate::ParsingState,
134 options: &ParseOptions,
135 ) -> ParseResult<Self> {
136 let (is_fragment, span) = match reader.next().ok_or_else(parse_lexing_error)? {
137 Token(TSXToken::JSXOpeningTagStart, span) => (false, span),
138 Token(TSXToken::JSXFragmentStart, span) => (true, span),
139 _ => panic!(),
140 };
141 Self::from_reader_sub_start(reader, state, options, is_fragment, span)
142 }
143
144 fn to_string_from_buffer<T: source_map::ToString>(
145 &self,
146 buf: &mut T,
147 options: &crate::ToStringOptions,
148 local: crate::LocalToStringInformation,
149 ) {
150 match self {
151 JSXRoot::Element(element) => element.to_string_from_buffer(buf, options, local),
152 JSXRoot::Fragment(fragment) => fragment.to_string_from_buffer(buf, options, local),
153 }
154 }
155
156 fn get_position(&self) -> Span {
157 match self {
158 JSXRoot::Element(element) => element.get_position(),
159 JSXRoot::Fragment(fragment) => fragment.get_position(),
160 }
161 }
162}
163
164fn parse_jsx_children(
165 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
166 state: &mut crate::ParsingState,
167 options: &ParseOptions,
168) -> ParseResult<Vec<JSXNode>> {
169 let mut children = Vec::new();
170 loop {
171 if matches!(
172 reader.peek(),
173 Some(Token(TSXToken::JSXFragmentEnd | TSXToken::JSXClosingTagStart, _))
174 ) {
175 return Ok(children);
176 }
177 children.push(JSXNode::from_reader(reader, state, options)?);
178 }
179}
180
181fn jsx_children_to_string<T: source_map::ToString>(
182 children: &[JSXNode],
183 buf: &mut T,
184 options: &crate::ToStringOptions,
185 local: crate::LocalToStringInformation,
186) {
187 let element_of_line_break_in_children =
188 children.iter().any(|node| matches!(node, JSXNode::Element(..) | JSXNode::LineBreak));
189
190 let mut previous_was_break = true;
191
192 for node in children {
193 if element_of_line_break_in_children
194 && !matches!(node, JSXNode::LineBreak)
195 && previous_was_break
196 {
197 options.add_indent(local.depth + 1, buf);
198 }
199 node.to_string_from_buffer(buf, options, local);
200 previous_was_break = matches!(node, JSXNode::Element(..) | JSXNode::LineBreak);
201 }
202
203 if options.pretty && local.depth > 0 && previous_was_break {
204 options.add_indent(local.depth, buf);
205 }
206}
207
208impl JSXRoot {
209 pub(crate) fn from_reader_sub_start(
210 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
211 state: &mut crate::ParsingState,
212 options: &ParseOptions,
213 is_fragment: bool,
214 start: TokenStart,
215 ) -> ParseResult<Self> {
216 if is_fragment {
217 Ok(Self::Fragment(JSXFragment::from_reader_sub_start(reader, state, options, start)?))
218 } else {
219 Ok(Self::Element(JSXElement::from_reader_sub_start(reader, state, options, start)?))
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Visitable)]
226#[apply(derive_ASTNode)]
227pub enum JSXNode {
228 Element(JSXElement),
229 TextNode(String, Span),
230 InterpolatedExpression(Box<FunctionArgument>, Span),
231 Comment(String, Span),
232 LineBreak,
233}
234
235impl ASTNode for JSXNode {
236 fn get_position(&self) -> Span {
237 match self {
238 JSXNode::TextNode(_, pos)
239 | JSXNode::InterpolatedExpression(_, pos)
240 | JSXNode::Comment(_, pos) => *pos,
241 JSXNode::Element(element) => element.get_position(),
242 JSXNode::LineBreak => source_map::Nullable::NULL,
243 }
244 }
245
246 fn from_reader(
247 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
248 state: &mut crate::ParsingState,
249 options: &ParseOptions,
250 ) -> ParseResult<Self> {
251 let token = reader.next().ok_or_else(parse_lexing_error)?;
252 match token {
253 Token(TSXToken::JSXContent(content), start) => {
254 let position = start.with_length(content.len());
255 Ok(JSXNode::TextNode(content.trim_start().into(), position))
257 }
258 Token(TSXToken::JSXExpressionStart, pos) => {
259 let expression = FunctionArgument::from_reader(reader, state, options)?;
260 let end_pos = reader.expect_next_get_end(TSXToken::JSXExpressionEnd)?;
261 Ok(JSXNode::InterpolatedExpression(Box::new(expression), pos.union(end_pos)))
262 }
263 Token(TSXToken::JSXOpeningTagStart, pos) => {
264 JSXElement::from_reader_sub_start(reader, state, options, pos).map(JSXNode::Element)
265 }
266 Token(TSXToken::JSXContentLineBreak, _) => Ok(JSXNode::LineBreak),
267 Token(TSXToken::JSXComment(comment), start) => {
268 let pos = start.with_length(comment.len() + 7);
269 Ok(JSXNode::Comment(comment, pos))
270 }
271 _token => Err(parse_lexing_error()),
272 }
273 }
274
275 fn to_string_from_buffer<T: source_map::ToString>(
276 &self,
277 buf: &mut T,
278 options: &crate::ToStringOptions,
279 local: crate::LocalToStringInformation,
280 ) {
281 match self {
282 JSXNode::Element(element) => {
283 element.to_string_from_buffer(buf, options, local.next_level());
284 }
285 JSXNode::TextNode(text, _) => buf.push_str(text),
286 JSXNode::InterpolatedExpression(expression, _) => {
287 buf.push('{');
288 expression.to_string_from_buffer(buf, options, local.next_level());
289 buf.push('}');
290 }
291 JSXNode::LineBreak => {
292 if options.pretty {
293 buf.push_new_line();
294 }
295 }
296 JSXNode::Comment(comment, _) => {
297 if options.pretty {
298 buf.push_str("<!--");
299 buf.push_str(comment);
300 buf.push_str("-->");
301 }
302 }
303 }
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Visitable)]
309#[apply(derive_ASTNode)]
310pub enum JSXAttribute {
311 Static(String, String, Span),
312 Dynamic(String, Box<Expression>, Span),
313 BooleanAttribute(String, Span),
314 Spread(Expression, Span),
315 Shorthand(Expression),
317}
318
319impl ASTNode for JSXAttribute {
320 fn get_position(&self) -> Span {
321 match self {
322 JSXAttribute::Static(_, _, pos)
323 | JSXAttribute::Dynamic(_, _, pos)
324 | JSXAttribute::BooleanAttribute(_, pos) => *pos,
325 JSXAttribute::Spread(_, spread_pos) => *spread_pos,
326 JSXAttribute::Shorthand(expr) => expr.get_position(),
327 }
328 }
329
330 fn from_reader(
331 _reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
332 _state: &mut crate::ParsingState,
333 _options: &ParseOptions,
334 ) -> ParseResult<Self> {
335 todo!("this is currently done in `JSXElement::from_reader`")
336 }
337
338 fn to_string_from_buffer<T: source_map::ToString>(
339 &self,
340 buf: &mut T,
341 options: &crate::ToStringOptions,
342 local: crate::LocalToStringInformation,
343 ) {
344 match self {
345 JSXAttribute::Static(key, expression, _) => {
346 buf.push_str(key.as_str());
347 buf.push('=');
348 buf.push('"');
349 buf.push_str(expression.as_str());
350 buf.push('"');
351 }
352 JSXAttribute::Dynamic(key, expression, _) => {
353 buf.push_str(key.as_str());
354 buf.push('=');
355 buf.push('{');
356 expression.to_string_from_buffer(buf, options, local);
357 buf.push('}');
358 }
359 JSXAttribute::BooleanAttribute(key, _) => {
360 buf.push_str(key.as_str());
361 }
362 JSXAttribute::Spread(expr, _) => {
363 buf.push_str("...");
364 expr.to_string_from_buffer(buf, options, local);
365 }
366 JSXAttribute::Shorthand(expr) => {
367 expr.to_string_from_buffer(buf, options, local);
368 }
369 }
370 }
371}
372
373impl JSXElement {
374 pub(crate) fn from_reader_sub_start(
375 reader: &mut impl TokenReader<TSXToken, crate::TokenStart>,
376 state: &mut crate::ParsingState,
377 options: &ParseOptions,
378 start: TokenStart,
379 ) -> ParseResult<Self> {
380 let Some(Token(TSXToken::JSXTagName(tag_name), _)) = reader.next() else {
381 return Err(parse_lexing_error());
382 };
383 let mut attributes = Vec::new();
384 while let Some(token) = reader.next() {
387 let (span, key) = match token {
388 Token(TSXToken::JSXOpeningTagEnd, _) => break,
390 t @ Token(TSXToken::JSXSelfClosingTag, _) => {
391 return Ok(JSXElement {
393 tag_name,
394 attributes,
395 children: JSXElementChildren::SelfClosing,
396 position: start.union(t.get_end()),
397 });
398 }
399 Token(TSXToken::JSXExpressionStart, _pos) => {
400 let attribute = if let Some(Token(TSXToken::Spread, _)) = reader.peek() {
401 let spread_token = reader.next().unwrap();
402 let expr = Expression::from_reader(reader, state, options)?;
403 reader.expect_next(TSXToken::CloseBrace)?;
404 JSXAttribute::Spread(expr, spread_token.get_span())
405 } else {
406 let expr = Expression::from_reader(reader, state, options)?;
407 JSXAttribute::Shorthand(expr)
408 };
409 attributes.push(attribute);
410 continue;
411 }
412 Token(TSXToken::JSXAttributeKey(key), start) => (start.with_length(key.len()), key),
413 _ => return Err(parse_lexing_error()),
414 };
415
416 if let Some(Token(TSXToken::JSXAttributeAssign, _)) = reader.peek() {
417 reader.next();
418 let attribute = match reader.next().unwrap() {
419 Token(TSXToken::JSXAttributeValue(expression), start) => {
420 let position = start.with_length(expression.len());
421 JSXAttribute::Static(key, expression, position)
422 }
423 Token(TSXToken::JSXExpressionStart, _) => {
424 let expression = Expression::from_reader(reader, state, options)?;
425 let close = reader.expect_next_get_end(TSXToken::JSXExpressionEnd)?;
426 JSXAttribute::Dynamic(key, Box::new(expression), span.union(close))
427 }
428 _ => return Err(parse_lexing_error()),
429 };
430 attributes.push(attribute);
431 } else {
432 attributes.push(JSXAttribute::BooleanAttribute(key, span));
434 }
435 }
436
437 let children = parse_jsx_children(reader, state, options)?;
438 if let Token(TSXToken::JSXClosingTagStart, _) =
439 reader.next().ok_or_else(parse_lexing_error)?
440 {
441 let end = if let Token(TSXToken::JSXClosingTagName(closing_tag_name), start) =
442 reader.next().ok_or_else(parse_lexing_error)?
443 {
444 let end =
445 start.0 + u32::try_from(closing_tag_name.len()).expect("4GB tag name") + 2;
446 if closing_tag_name != tag_name {
447 return Err(ParseError::new(
448 crate::ParseErrors::ClosingTagDoesNotMatch {
449 expected: &tag_name,
450 found: &closing_tag_name,
451 },
452 start.with_length(closing_tag_name.len() + 2),
453 ));
454 }
455 TokenEnd::new(end)
456 } else {
457 return Err(parse_lexing_error());
458 };
459 Ok(JSXElement {
460 tag_name,
461 attributes,
462 children: JSXElementChildren::Children(children),
463 position: start.union(end),
464 })
465 } else {
466 Err(parse_lexing_error())
467 }
468 }
469}
470
471#[must_use]
473pub fn html_tag_contains_literal_content(tag_name: &str) -> bool {
474 matches!(tag_name, "script" | "style")
475}
476
477#[must_use]
479pub fn html_tag_is_self_closing(tag_name: &str) -> bool {
480 matches!(
481 tag_name,
482 "area"
483 | "base" | "br"
484 | "col" | "embed"
485 | "hr" | "img"
486 | "input" | "link"
487 | "meta" | "param"
488 | "source"
489 | "track" | "wbr"
490 )
491}