ezno_parser/extensions/
jsx.rs

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	/// Name of the element (TODO or reference to element)
21	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	/// For img elements
32	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// TODO can `JSXFragment` appear here?
225#[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				// TODO `trim` debatable
256				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/// TODO spread attributes and boolean attributes
308#[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	/// Preferably want a identifier here not an expr
316	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		// TODO spread attributes
385		// Kind of weird / not clear conditions for breaking out of while loop
386		while let Some(token) = reader.next() {
387			let (span, key) = match token {
388				// Break here
389				Token(TSXToken::JSXOpeningTagEnd, _) => break,
390				t @ Token(TSXToken::JSXSelfClosingTag, _) => {
391					// Early return if self closing
392					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				// Boolean attributes
433				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/// Used for lexing
472#[must_use]
473pub fn html_tag_contains_literal_content(tag_name: &str) -> bool {
474	matches!(tag_name, "script" | "style")
475}
476
477/// Used for lexing
478#[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}