const std = @import("std"); const logger = @import("../logger.zig"); const mdx_lexer = @import("./mdx_lexer.zig"); const Lexer = mdx_lexer.Lexer; const importRecord = @import("../import_record.zig"); const js_ast = @import("../js_ast.zig"); const JSParser = @import("../js_parser/js_parser.zig").MDXParser; const ParseStatementOptions = @import("../js_parser/js_parser.zig").ParseStatementOptions; const options = @import("../options.zig"); const fs = @import("../fs.zig"); const _global = @import("../global.zig"); const string = _global.string; const Output = _global.Output; const Global = _global.Global; const Environment = _global.Environment; const strings = _global.strings; const MutableString = _global.MutableString; const stringZ = _global.stringZ; const default_allocator = _global.default_allocator; const C = _global.C; const expect = std.testing.expect; const ImportKind = importRecord.ImportKind; const BindingNodeIndex = js_ast.BindingNodeIndex; const Define = @import("../defines.zig").Define; const js_lexer = @import("../js_lexer.zig"); const StmtNodeIndex = js_ast.StmtNodeIndex; const ExprNodeIndex = js_ast.ExprNodeIndex; const ExprNodeList = js_ast.ExprNodeList; const StmtNodeList = js_ast.StmtNodeList; const BindingNodeList = js_ast.BindingNodeList; const ParserOptions = @import("../js_parser/js_parser.zig").Parser.Options; const runVisitPassAndFinish = @import("../js_parser/js_parser.zig").Parser.runVisitPassAndFinish; const Ref = @import("../ast/base.zig").Ref; const assert = std.debug.assert; const BabyList = js_ast.BabyList; const LocRef = js_ast.LocRef; const S = js_ast.S; const B = js_ast.B; const G = js_ast.G; const T = mdx_lexer.T; const E = js_ast.E; const Stmt = js_ast.Stmt; const Expr = js_ast.Expr; const Binding = js_ast.Binding; const Symbol = js_ast.Symbol; const Level = js_ast.Op.Level; const Op = js_ast.Op; const Scope = js_ast.Scope; const Range = logger.Range; pub const Container = struct { ch: u8 = 0, is_loose: bool = false, is_task: bool = false, start: u32 = 0, mark_indent: u32 = 0, contents_indent: u32 = 0, block_index: u32 = 0, task_mark_off: u32 = 0, }; pub const Block = struct { tag: Tag = Tag.html, flags: Block.Flags.Set = Block.Flags.Set{}, data: u32 = 0, /// Leaf blocks: Count of lines (MD_LINE or MD_VERBATIMLINE) on the block. /// LI: Task mark offset in the input doc. /// OL: Start item number. /// line_count: u32 = 0, line_offset: u32 = 0, detail: Block.Detail = Block.Detail{ .none = .{} }, pub inline fn lines(this: Block, lines_: BabyList(Line)) []Line { return lines_.ptr[this.line_offset .. this.line_offset + this.line_count]; } pub inline fn verbatimLines(this: Block, lines_: BabyList(Line.Verbatim)) []Line.Verbatim { return lines_.ptr[this.line_offset .. this.line_offset + this.line_count]; } pub const Data = u32; pub const Flags = enum(u3) { container_opener = 0, container_closer = 1, loose_list = 2, setext_header = 3, pub const Set = std.enums.EnumSet(Block.Flags); }; pub inline fn isContainer(this: Block) bool { return this.flags.contains(.container_opener) or this.flags.contains(.container_closer); } pub const Tag = enum { /// ... doc, ///
...
quote, /// ///Detail: Structure ul_detail. ul, ///
    ...
///Detail: Structure ol_detail. ol, ///
  • ...
  • ///Detail: Structure li_detail. li, ///
    hr, ///

    ...

    (for levels up to 6) ///Detail: Structure h_detail. h, ///
    ...
    ///Note the text lines within code blocks are terminated with '\n' ///instead of explicit MD_TEXT_BR. code, /// Raw HTML block. This itself does not correspond to any particular HTML ///tag. The contents of it _is_ raw HTML source intended to be put ///in verbatim form to the HTML output. html, ///

    ...

    p, /// ...
    and its contents. ///Detail: Structure table_detail (for table), /// structure td_detail (for th and td) ///Note all of these are used only if extension MD_FLAG_TABLES is enabled. table, thead, tbody, tr, th, td, }; pub const UL = struct { tight: bool = false, mark: u8 = '*', }; pub const OL = struct { start: u32 = 0, tight: bool = false, mark: u8 = '*', }; pub const LI = struct { /// Can be non-zero only with MD_FLAG_TASKLISTS task: bool = false, /// is_task, then one of 'x', 'X' or ' '. Undefined otherwise. task_mark: u8 = 'x', /// If is_task, then offset in the input of the char between '[' and ']'. task_mark_off: u32 = 0, }; pub const Header = u4; pub const Code = struct { info: Attribute = .{}, lang: Attribute = .{}, /// character used for fenced code block; or zero for indented code block. * fence: u8 = '`', }; pub const Table = struct { /// Count of columns in the table. column_count: u32 = 0, /// Count of rows in the table header (currently always 1) head_row_count: u32 = 1, /// Count of rows in the table body body_row_count: u32 = 0, }; pub const Detail = union { none: void, ul: UL, ol: OL, li: LI, }; pub const TD = struct { alignment: Align = Align.default, }; }; pub const Span = struct { pub const Tag = enum { /// ... em, /// ... strong, /// ... /// Detail: Structure a_detail. a, /// ... /// Detail: Structure img_detail. /// Note: Image text can contain nested spans and even nested images. /// If rendered into ALT attribute of HTML tag, it's responsibility /// of the parser to deal with it. img, /// ... code, /// ... /// Note: Recognized only when MD_FLAG_STRIKETHROUGH is enabled. del, /// For recognizing inline ($) and display ($$) equations /// Note: Recognized only when MD_FLAG_LATEXMATHSPANS is enabled. latexmath, latexmath_display, /// Wiki links /// Note: Recognized only when MD_FLAG_WIKILINKS is enabled. wikilink, /// ... /// Note: Recognized only when MD_FLAG_UNDERLINE is enabled. u, }; pub const Link = struct { src: Attribute = .{}, title: Attribute = .{}, }; pub const Image = Link; pub const Wikilink = struct { target: Attribute = .{}, }; }; pub const Text = enum { /// Normal text. normal, /// NULL character. CommonMark requires replacing NULL character with /// the replacement char U+FFFD, so this allows caller to do that easily. nullchar, /// Line breaks. /// Note these are not sent from blocks with verbatim output (MD_BLOCK_CODE /// or MD_BLOCK_HTML). In such cases, '\n' is part of the text itself. ///
    (hard break) br, /// '\n' in source text where it is not semantically meaningful (soft break) softbr, /// Entity. /// (a) Named entity, e.g.   /// (Note MD4C does not have a list of known entities. /// Anything matching the regexp /&[A-Za-z][A-Za-z0-9]{1,47};/ is /// treated as a named entity.) /// (b) Numerical entity, e.g. Ӓ /// (c) Hexadecimal entity, e.g. ካ /// /// As MD4C is mostly encoding agnostic, application gets the verbatim /// entity text into the MD_PARSER::text_callback(). entity, /// Text in a code block (inside MD_BLOCK_CODE) or inlined code (`code`). /// If it is inside MD_BLOCK_CODE, it includes spaces for indentation and /// '\n' for new lines. br and softbr are not sent for this /// kind of text. code, /// Text is a raw HTML. If it is contents of a raw HTML block (i.e. not /// an inline raw HTML), then br and softbr are not used. /// The text contains verbatim '\n' for the new lines. html, /// Text is inside an equation. This is processed the same way as inlined code /// spans (`code`). latexmath, }; pub const Align = enum(u3) { default = 0, left = 1, center = 2, right = 3, }; /// String attribute. /// /// This wraps strings which are outside of a normal text flow and which are /// propagated within various detailed structures, but which still may contain /// string portions of different types like e.g. entities. /// /// So, for example, lets consider this image: /// /// ![image alt text](http://example.org/image.png 'foo " bar') /// /// The image alt text is propagated as a normal text via the MD_PARSER::text() /// callback. However, the image title ('foo " bar') is propagated as /// MD_ATTRIBUTE in MD_SPAN_IMG_DETAIL::title. /// /// Then the attribute MD_SPAN_IMG_DETAIL::title shall provide the following: /// -- [0]: "foo " (substr_types[0] == MD_TEXT_NORMAL; substr_offsets[0] == 0) /// -- [1]: """ (substr_types[1] == MD_TEXT_ENTITY; substr_offsets[1] == 4) /// -- [2]: " bar" (substr_types[2] == MD_TEXT_NORMAL; substr_offsets[2] == 10) /// -- [3]: (n/a) (n/a ; substr_offsets[3] == 14) /// /// Note that these invariants are always guaranteed: /// -- substr_offsets[0] == 0 /// -- substr_offsets[LAST+1] == size /// -- Currently, only MD_TEXT_NORMAL, MD_TEXT_ENTITY, MD_TEXT_NULLCHAR /// substrings can appear. This could change only of the specification /// changes. /// pub const Attribute = struct { text: []const u8 = "", substring: Substring.List = .{}, }; pub const Substring = struct { offset: u32, tag: Text, pub const List = std.MultiArrayList(Substring); pub const ListPool = ObjectPool(List); }; pub const Mark = struct { position: Ref = Ref{}, prev: u32 = std.math.maxInt(u32), next: u32 = std.math.maxInt(u32), ch: u8 = 0, flags: u16 = 0, /// Maybe closer. pub const potential_closer = 0x02; /// Maybe opener. pub const potential_opener = 0x01; /// Definitely opener. pub const opener = 0x04; /// Definitely closer. pub const closer = 0x08; /// Resolved in any definite way. pub const resolved = 0x10; /// Helper for the "rule of 3". */ pub const emph_intraword = 0x20; pub const emph_mod3_0 = 0x40; pub const emph_mod3_1 = 0x80; pub const emph_mod3_2 = (0x40 | 0x80); pub const emph_mod3_mask = (0x40 | 0x80); /// Distinguisher for '<', '>'. */ pub const autolink = 0x20; /// For permissive autolinks. */ pub const validpermissiveautolink = 0x20; /// For '[' to rule out invalid link labels early */ pub const hasnestedbrackets = 0x20; /// During analyzes of inline marks, we need to manage some "mark chains", /// of (yet unresolved) openers. This structure holds start/end of the chain. /// The chain internals are then realized through MD_MARK::prev and ::next. pub const Chain = struct { head: u32 = std.math.maxInt(u32), tail: u32 = std.math.maxInt(u32), pub const List = struct { data: [13]Chain = [13]Chain{ .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{} }, pub inline fn ptr_chain(this: *List) *Chain { return &this.data[0]; } pub inline fn tablecellboundaries(this: *List) *Chain { return &this.data[1]; } pub inline fn asterisk_openers_extraword_mod3_0(this: *List) *Chain { return &this.data[2]; } pub inline fn asterisk_openers_extraword_mod3_1(this: *List) *Chain { return &this.data[3]; } pub inline fn asterisk_openers_extraword_mod3_2(this: *List) *Chain { return &this.data[4]; } pub inline fn asterisk_openers_intraword_mod3_0(this: *List) *Chain { return &this.data[5]; } pub inline fn asterisk_openers_intraword_mod3_1(this: *List) *Chain { return &this.data[6]; } pub inline fn asterisk_openers_intraword_mod3_2(this: *List) *Chain { return &this.data[7]; } pub inline fn underscore_openers(this: *List) *Chain { return &this.data[8]; } pub inline fn tilde_openers_1(this: *List) *Chain { return &this.data[9]; } pub inline fn tilde_openers_2(this: *List) *Chain { return &this.data[10]; } pub inline fn bracket_openers(this: *List) *Chain { return &this.data[11]; } pub inline fn dollar_openers(this: *List) *Chain { return &this.data[12]; } }; }; }; pub const Line = struct { beg: u32 = 0, end: u32 = 0, pub const Tag = enum(u32) { blank, hr, atx_header, setext_header, setext_underline, indented_code, fenced_code, html, text, table, table_underline, }; pub const Analysis = packed struct { tag: Tag = Tag.blank, beg: u32 = 0, end: u32 = 0, indent: u32 = 0, data: u32 = 0, pub const blank = Analysis{}; pub fn eql(a: Analysis, b: Analysis) bool { return strings.eqlLong(std.mem.asBytes(&a), std.mem.asBytes(&b), false); } }; pub const Verbatim = struct { line: Line = Line{}, indent: u32 = 0, }; }; pub const MDParser = struct { marks: BabyList(Mark) = .{}, chain: Mark.Chain.List = .{}, source: logger.Source, flags: Flags.Set = Flags.commonmark, allocator: std.mem.Allocator, mdx: *MDX, mark_char_map: [255]u1 = undefined, doc_ends_with_newline: bool = false, size: u32 = 0, lines: BabyList(Line) = .{}, verbatim_lines: BabyList(Line.Verbatim) = .{}, containers: BabyList(Container) = .{}, blocks: BabyList(Block) = .{}, current_block: ?*Block = null, current_block_index: u32 = 0, code_fence_length: u32 = 0, code_indent_offset: u32 = std.math.maxInt(u32), last_line_has_list_loosening_effect: bool = false, last_list_item_starts_with_two_blank_lines: bool = false, pub const Flags = enum { /// In MD_TEXT_NORMAL, collapse non-trivial whitespace into single ' ' collapse_whitespace, /// Do not require space in ATX headers ( ###header ) permissive_atxheaders, /// Recognize URLs as autolinks even without '<', '>' permissive_url_autolinks, /// Recognize e-mails as autolinks even without '<', '>' and 'mailto:' permissive_email_autolinks, /// Disable indented code blocks. (Only fenced code works.) noindented_codeblocks, /// Disable raw HTML blocks. no_html_blocks, /// Disable raw HTML (inline). no_html_spans, /// Enable tables extension. tables, /// Enable strikethrough extension. strikethrough, /// Enable WWW autolinks (even without any scheme prefix, if they begin with 'www.') permissive_www_autolinks, /// Enable task list extension. tasklists, /// Enable $ and $$ containing LaTeX equations. latex_mathspans, /// Enable wiki links extension. wikilinks, /// Enable underline extension (and disables '_' for normal emphasis). underline, pub const Set = std.enums.EnumSet(Flags); pub const permissive_autolinks = Set.init(.{ .permissive_email_autolinks = true, .permissive_url_autolinks = true }); pub const no_email = Set.init(.{ .no_html_blocks = true, .no_html_spans = true }); pub const github = Set.init(.{ .tables = true, .permissive_autolinks = true, .strikethrough = true, .tasklists = true }); pub const commonmark: i32 = Set{}; }; fn buildCharMap(this: *MDParser) void { @memset(&this.mark_char_map, 0, this.mark_char_map.len); this.mark_char_map['\\'] = 1; this.mark_char_map['*'] = 1; this.mark_char_map['_'] = 1; this.mark_char_map['`'] = 1; this.mark_char_map['&'] = 1; this.mark_char_map[';'] = 1; this.mark_char_map['<'] = 1; this.mark_char_map['>'] = 1; this.mark_char_map['['] = 1; this.mark_char_map['!'] = 1; this.mark_char_map[']'] = 1; this.mark_char_map[0] = 1; // whitespace this.mark_char_map[' '] = 1; this.mark_char_map['\t'] = 1; this.mark_char_map['\r'] = 1; this.mark_char_map['\n'] = 1; // form feed this.mark_char_map[0xC] = 1; // vertical tab this.mark_char_map[0xB] = 1; if (this.flags.contains(.strikethrough)) { this.mark_char_map['~'] = 1; } if (this.flags.contains(.latex_mathspans)) { this.mark_char_map['$'] = 1; } if (this.flags.contains(.permissive_email_autolinks)) { this.mark_char_map['@'] = 1; } if (this.flags.contains(.permissive_url_autolinks)) { this.mark_char_map[':'] = 1; } if (this.flags.contains(.permissive_www_autolinks)) { this.mark_char_map['.'] = 1; } if (this.flags.contains(.tables)) { this.mark_char_map['.'] = 1; } } pub fn init(allocator: std.mem.Allocator, source: logger.Source, flags: Flags.Set, mdx: *MDX) MDParser { var parser = MDParser{ .allocator = allocator, .source = source, .flags = flags, .mdx = mdx, .size = @truncate(u32, source.contents.len), }; parser.buildCharMap(); parser.doc_ends_with_newline = source.contents.len.len > 0 and source.contents[source.contents.len - 1] == '\n'; return parser; } fn startNewBlock(this: *MDParser, line: *const Line.Analysis) !void { try this.blocks.push( this.allocator, Block{ .tag = switch (line.tag) { .hr => Block.Tag.hr, .atx_header, .setext_header => Block.Tag.h, .fenced_code, .indented_code => Block.Tag.code, .text => Block.Tag.p, .html => Block.Tag.html, else => unreachable, }, .data = line.data, .line_count = 0, .line_offset = switch (line.tag) { .indented_code, .html, .fenced_code => this.verbatim_lines.len, else => this.lines.len, }, }, ); } inline fn charAt(this: *const MDParser, index: u32) u8 { return this.source.contents[index]; } inline fn isNewline(this: *const MDParser, index: u32) bool { return switch (this.charAt(index)) { '\n', '\r' => true, else => false, }; } inline fn isAnyOf2(this: *const MDParser, index: u32, comptime first: u8, comptime second: u8) bool { return isAnyOf2_(this.charAt(index), first, second); } inline fn isAnyOf2_(char: u8, comptime first: u8, comptime second: u8) bool { return switch (char) { first, second => true, else => false, }; } inline fn isAnyOf(this: *const MDParser, index: u32, comptime values: []const u8) bool { return isCharAnyOf(this.charAt(index), values); } inline fn isCharAnyOf(char: u8, comptime values: []const u8) bool { inline for (values) |val| { if (val == char) return true; } return false; } inline fn isBlank(char: u8) bool { return isCharAnyOf(char, &[_]u8{ ' ', '\t' }); } inline fn isWhitespace(char: u8) bool { return isCharAnyOf(char, &[_]u8{ ' ', '\t', 0xC, 0xB }); } pub fn getIndent(this: *MDParser, total_indent: u32, beg: u32, end: *u32) u32 { var off = beg; var indent = total_indent; while (off < this.size and isBlank(this.charAt(off))) { if (this.charAt(off) == '\t') { indent = (indent + 4) & ~3; } else { indent += 1; } off += 1; } end.* = off; return indent - total_indent; } pub fn isContainerMark(this: *MDParser, indent: u32, beg: u32, end: *u32, container: *Container) bool { var off = beg; var max_end: u32 = undefined; if (off >= this.size or indent >= this.code_indent_offset) return false; if (this.charAt(off) == '>') { off += 1; container.ch = '>'; container.is_loose = false; container.is_task = false; container.mark_indent = indent; container.contents_indent = indent + 1; end.* = off; return true; } // Check for list item bullet mark. if (this.isAnyOf(off, "-+*") and (off + 1 >= this.size or isBlank(this.charAt(off + 1)) or this.isNewline(off + 1))) { container.ch = this.charAt(off); container.is_loose = false; container.is_task = false; container.mark_indent = indent; container.contents_indent = indent + 1; end.* = off + 1; return true; } // Check for ordered list item marks max_end = @minimum(off + 9, this.size); container.start = 0; while (off < max_end and std.ascii.isDigit(this.charAt(off))) { container.start = container.start * 10 + (this.charAt(off) - '0'); off += 1; } if (off > beg and off < this.size and (this.isAnyOf2(off, '.', ')')) and (off + 1 >= this.size or this.isBlank(this.charAt(off + 1) or this.isNewline(off + 1)))) { container.ch = this.charAt(off); container.is_loose = false; container.is_task = false; container.mark_indent = indent; container.contents_indent = indent + off - beg + 1; end.* = off + 1; return true; } return false; } fn analyzeLine(this: *MDParser, beg: u32, end: *u32, pivot_line: *const Line.Analysis, line: *Line.Analysis) !void { _ = this; _ = beg; _ = end; _ = pivot_line; _ = line; var off = beg; var hr_killer: u32 = 0; var prev_line_has_list_loosening_effect = this.last_line_has_list_loosening_effect; var container = Container{}; _ = hr_killer; _ = prev_line_has_list_loosening_effect; _ = container; var total_indent: u32 = 0; var n_parents: u32 = 0; var n_brothers: u32 = 0; var n_children: u32 = 0; // Given the indentation and block quote marks '>', determine how many of // the current containers are our parents. while (n_parents < this.containers.len) { var c: *Container = this.containers.ptr + n_parents; if (c.ch == '>' and line.indent < this.code_indent_offset and off < this.size and this.charAt(off) == '>') { off += 1; total_indent += 1; line.indent = this.getIndent(total_indent, off, &off); total_indent += line.indent; // The optional 1st space after '>' is part of the block quote mark. line.indent -|= line.indent; line.beg = off; } else if (c.ch != '>' and line.indent >= c.contents_indent) { line.indent -|= c.contents_indent; } else { break; } n_parents += 1; } if (off >= this.size or this.isNewline(off)) { // Blank line does not need any real indentation to be nested inside a list if (n_brothers + n_children == 0) { while (n_parents < this.containers.len and this.containers.ptr[n_parents].ch == '>') { n_parents += 1; } } } while (true) { switch (pivot_line.tag) { .fencedcode => { // Check whether we are fenced code continuation. line.beg = off; // We are another MD_LINE_FENCEDCODE unless we are closing fence // which we transform into MD_LINE_BLANK. if (line.indent < this.code_indent_offset) { if (this.isClosingCodeFence(this.charAt(pivot_line.beg), off, &off)) { line.tag = .blank; this.last_line_has_list_loosening_effect = false; break; } } // Change indentation accordingly to the initial code fence. if (n_parents == this.containers.len) { line.indent -|= pivot_line.indent; line.tag = .fenced_code; break; } }, .indentedcode => {}, .text => {}, .html => {}, else => {}, } // Check for blank line. if (off >= this.size or this.isNewline(off)) { if (pivot_line.tag == .indented_code and n_parents == this.containers.len) { line.tag = .indented_code; line.indent -|= this.code_indent_offset; this.last_line_has_list_loosening_effect = false; } else { line.tag = .blank; this.last_line_has_list_loosening_effect = n_parents > 0 and n_brothers + n_children == 0 and this.containers.ptr[n_parents - 1].ch != '>'; // See https://github.com/mity/md4c/issues/6 // // This ugly checking tests we are in (yet empty) list item but // not its very first line (i.e. not the line with the list // item mark). // // If we are such a blank line, then any following non-blank // line which would be part of the list item actually has to // end the list because according to the specification, "a list // item can begin with at most one blank line." // if (n_parents > 0 and this.containers.ptr[n_parents - 1].ch != '>' and n_brothers + n_children == 0 and this.current_block == null and this.blocks.len > 0) { var top_block = this.blocks.last().?; if (top_block.tag == .li) { this.last_list_item_starts_with_two_blank_lines = true; } } } break; } else { // This is the 2nd half of the hack. If the flag is set (i.e. there // was a 2nd blank line at the beginning of the list item) and if // we would otherwise still belong to the list item, we enforce // the end of the list. this.last_line_has_list_loosening_effect = false; if (this.last_list_item_starts_with_two_blank_lines) { if (n_parents > 0 and this.containers.ptr[n_parents - 1].ch != '>' and n_brothers + n_children == 0 and this.current_block == null and this.blocks.len > 1) { var top = this.blocks.last().?; if (top.tag == .li) { n_parents -|= 1; } } this.last_line_has_list_loosening_effect = true; } } // Check whether we are Setext underline. if (line.indent < this.code_indent_offset and pivot_line.tag == .text and off < this.size and this.isAnyOf2(off, '=', '-') and n_parents == this.containers.len) { var level: u4 = 0; if (this.isSetextUnderline(off, &off, &level)) { line.tag = .setext_underline; line.data = level; break; } } // Check for a thematic break line if (line.indent < this.code_indent_offset and off < this.size and off >= hr_killer and this.isAnyOf(off, "-_*")) { if (this.isHRLine(off, &off, &hr_killer)) { line.tag = .hr; break; } } // Check for "brother" container. I.e. whether we are another list item //in already started list. if (n_parents < this.containers.len and n_brothers + n_children == 0) { var tmp: u32 = undefined; if (this.isContainerMark(line.indent, off, &tmp, &container) and isContainerCompatible(&this.containers.ptr[n_parents], &container)) { pivot_line.* = Line.Analysis.blank; off = tmp; total_indent += container.contents_indent - container.mark_indent; line.indent = this.getIndent(total_indent, off, &off); total_indent += line.indent; line.beg = off; // Some of the following whitespace actually still belongs to the mark. if (off >= this.size or this.isNewline(off)) { container.contents_indent += 1; } else if (line.indent <= this.code_indent_offset) { container.contents_indent += line.indent; line.indent = 0; } else { container.contents_indent += 1; line.indent -= 1; } this.containers.ptr[n_parents].mark_indent = container.mark_indent; this.containers.ptr[n_parents].contents_indent = container.contents_indent; n_brothers += 1; continue; } } // Check for indented code // Note: indented code block cannot interrupt a paragrpah if (line.indent >= this.code_indent_offset and (pivot_line.tag == .blank or pivot_line.tag == .indented_code)) { line.tag = .indented_code; std.debug.assert(line.indent >= this.code_indent_offset); line.indent -|= this.code_indent_offset; line.data = 0; break; } // Check for start of a new container block if (line.indent < this.code_indent_offset and this.isContainerMark(line.indent, off, &off, &container)) { if (pivot_line.tag == .text and n_parents == this.n_containers and (off >= this.size or this.isNewline(off)) and container.ch != '>') { // Noop. List mark followed by a blank line cannot interrupt a paragraph. } else if (pivot_line.tag == .text and n_parents == this.containers.len and isAnyOf2_(container.ch, '.', ')')) { // Noop. Ordered list cannot interrupt a paragraph unless the start index is 1. } else { total_indent += container.contents_indent - container.mark_indent; line.indent = this.getIndent(total_indent, off, &off); total_indent += line.indent; line.beg = off; line.data = container.ch; // Some of the following whitespace actually still belongs to the mark. if (off >= this.size or this.isNewline(off)) { container.contents_indent += 1; } else if (line.indent <= this.code_indent_offset) { container.contents_indent += line.indent; line.indent = 0; } else { container.contents_indent += 1; line.indent -= 1; } if (n_brothers + n_children == 0) { pivot_line.* = Line.Analysis.blank; } if (n_children == 0) { try this.leaveChildContainers(n_parents + n_brothers); } n_children += 1; try this.pushContainer(container); continue; } } // heck whether we are table continuation. if (pivot_line.tag == .table and n_parents == this.n_containers) { line.tag = .table; break; } // heck for ATX header. if (line.indent < this.code_indent_offset and off < this.size and this.isAnyOf(off, '#')) { var level: u4 = 0; if (this.isATXHeaderLine(off, &line.beg, &off, &level)) { line.tag = .atx_header; line.data = level; break; } } // Check whether we are starting code fence. if (off < this.size and this.isAnyOf2(off, '`', '~')) { if (this.isOpeningCodeFence(off, &off)) { line.tag = .fenced_code; line.data = 1; break; } } // Check for start of raw HTML block. if (off < this.size and !this.flags.contains(.no_html_blocks) and this.charAt(off) == '<') {} // Check for table underline. if (this.flags.contains(.tables) and pivot_line.tag == .text and off < this.size and this.isAnyOf(off, "|-:") and n_parents == this.containers.len) { var col_count: u32 = undefined; if (this.current_block != null and this.current_block.?.line_count == 1 and this.isTableUnderline(off, &off, &col_count)) { line.data = col_count; line.tag = .table_underline; break; } } // By default, we are normal text line. line.tag = .text; if (pivot_line.tag == .text and n_brothers + n_children == 0) { // lazy continuation n_parents = this.containers.len; } // Check for task mark. if (this.flags.contains(.tasklists) and n_brothers + n_children > 0 and off < this.size and isCharAnyOf(this.containers.last().?.ch, "-+*.)")) { var tmp: u32 = off; while (tmp < this.size and tmp < off + 3 and isBlank(tmp)) { tmp += 1; } if ((tmp + 2 < this.size and this.charAt(tmp) == '[' and this.isAnyOf(tmp + 1, "xX ") and this.charAt(tmp + 2) == ']') and (tmp + 3 == this.size or isBlank(this.charAt(tmp + 3)) or this.isNewline(tmp + 3))) { var task_container: *Container = if (n_children > 0) this.containers.last().? else &container; task_container.is_task = true; task_container.task_mark_off = tmp + 1; off = tmp + 3; while (off < this.size and isWhitespace(this.charAt(off))) { off += 1; } if (off == this.size) break; line.beg = off; } } break; } // Scan for end of the line. while (off + 3 < this.size and !(strings.eqlComptimeIgnoreLen(this.source.contents.ptr[off..][0..4], "\n\n\n\n") or strings.eqlComptimeIgnoreLen(this.source.contents.ptr[off..][0..4], "\r\n\r\n"))) { off += 4; } while (off < this.size and !this.isNewline(off)) { off += 1; } // Set end of line line.end = off; // ut for ATX header, we should exclude the optional trailing mark. if (line.type == .atx_header) { var tmp = line.end; while (tmp > line.beg and this.charAt(tmp - 1) == ' ') { tmp -= 1; } while (tmp > line.beg and this.charAt(tmp - 1) == '#') { tmp -= 1; } if (tmp == line.beg or this.charAt(tmp - 1) == ' ' or this.flags.contains(.permissive_atxheaders)) { line.end = tmp; } } // Trim trailing spaces. switch (line.tag) { .indented_code, .fenced_code => {}, else => { while (line.end > line.beg and this.charAt(line.end - 1) == ' ') { line.end -= 1; } }, } // Eat also the new line if (off < this.size and this.charAt(off) == '\r') { off += 1; } if (off < this.size and this.charAt(off) == '\n') { off += 1; } end.* = off; // If we belong to a list after seeing a blank line, the list is loose. if (prev_line_has_list_loosening_effect and line.tag != .blank and n_parents + n_brothers > 0) { var c: *Container = this.containers.ptr[n_parents + n_brothers - 1]; if (c.ch != '>') { var block: *Block = this.blocks.ptr[c.block_index]; block.flags.insert(.loose_list); } } // Leave any containers we are not part of anymore. if (n_children == 0 and n_parents + n_brothers < this.containers.len) { try this.leaveChildContainers(n_parents + n_brothers); } // Enter any container we found a mark for if (n_brothers > 0) { std.debug.assert(n_brothers == 0); try this.pushContainerBytes( Block.Tag.li, this.containers.ptr[n_parents].task_mark_off, if (this.containers.ptr[n_parents].is_task) this.charAt(this.containers.ptr[n_parents].task_mark_off) else 0, Block.Flags.container_closer, ); try this.pushContainerBytes( Block.Tag.li, container.task_mark_off, if (container.is_task) this.charAt(container.task_mark_off) else 0, Block.Flags.container_opener, ); this.containers.ptr[n_parents].is_task = container.is_task; this.containers.ptr[n_parents].task_mark_off = container.task_mark_off; } if (n_children > 0) { try this.enterChildContainers(n_children); } } fn processLine(this: *MDParser, p_pivot_line: **const Line.Analysis, line: *Line.Analysis) !void { var pivot_line = p_pivot_line.*; switch (line.tag) { .blank => { // Blank line ends current leaf block. try this.endCurrentBlock(); p_pivot_line.* = Line.Analysis.blank; }, .hr, .atx_header => { try this.endCurrentBlock(); // Add our single-line block try this.startNewBlock(line); try this.addLineIntoCurrentBlock(line); try this.endCurrentBlock(); p_pivot_line.* = &Line.Analysis.blank; }, .setext_underline => { this.current_block.?.tag = .table; this.current_block.?.data = line.data; this.current_block.?.flags.insert(.setext_header); try this.addLineIntoCurrentBlock(line); try this.endCurrentBlock(); if (this.current_block == null) { p_pivot_line.* = &Line.Analysis.blank; } else { // This happens if we have consumed all the body as link ref. defs. //and downgraded the underline into start of a new paragraph block. line.tag = .text; p_pivot_line.* = line; } }, // MD_LINE_TABLEUNDERLINE changes meaning of the current block. .table_underline => { var current_block = this.current_block.?; std.debug.assert(current_block.line_count == 1); current_block.tag = .table; current_block.data = line.data; std.debug.assert(pivot_line != &Line.Analysis.blank); @intToPtr(*Line.Analysis, @ptrToInt(p_pivot_line.*)).tag = .table; try this.addLineIntoCurrentBlock(line); }, else => { // The current block also ends if the line has different type. if (line.tag != pivot_line.tag) { try this.endCurrentBlock(); } // The current line may start a new block. if (this.current_block == null) { try this.startNewBlock(line); p_pivot_line.* = line; } // In all other cases the line is just a continuation of the current block. try this.addLineIntoCurrentBlock(line); }, } } fn consumeLinkReferenceDefinitions(this: *MDParser) !void { _ = this; } fn addLineIntoCurrentBlock(this: *MDParser, analysis: *const Line.Analysis) !void { var current_block = this.current_block.?; switch (current_block.tag) { .code, .html => { if (current_block.line_count > 0) std.debug.assert( this.verbatim_lines.len == current_block.line_count + current_block.line_offset, ); if (current_block.line_count == 0) { current_block.line_offset = this.verbatim_lines.len; } try this.verbatim_lines.push(this.allocator, Line.Verbatim{ .indent = analysis.indent, .line = .{ .beg = analysis.beg, .end = analysis.end, }, }); }, else => { if (current_block.line_count > 0) std.debug.assert( this.lines.len == current_block.line_count + current_block.line_offset, ); if (current_block.line_count == 0) { current_block.line_offset = this.lines.len; } this.lines.push(this.allocator, .{ .beg = analysis.beg, .end = analysis.end }); }, } current_block.line_count += 1; } fn endCurrentBlock(this: *MDParser) !void { _ = this; var block = this.current_block orelse return; // Check whether there is a reference definition. (We do this here instead // of in md_analyze_line() because reference definition can take multiple // lines.) */ if ((block.tag == .p or block.tag == .h) and block.flags.contains(.setext_header)) { var lines = block.lines(this.lines); if (lines[0].beg == '[') { try this.consumeLinkReferenceDefinitions(); block = this.current_block orelse return; } } if (block.tag == .h and block.flags.contains(.setext_header)) { var n_lines = block.line_count; if (n_lines > 1) { // get rid of the underline if (this.lines.len == block.line_count + block.line_offset) { this.lines.len -= 1; } block.line_count -= 1; } else { // Only the underline has left after eating the ref. defs. // Keep the line as beginning of a new ordinary paragraph. */ block.tag = .p; } } // Mark we are not building any block anymore. this.current_block = null; this.current_block_index -|= 1; } fn buildRefDefHashTable(this: *MDParser) !void { _ = this; } fn leaveChildContainers(this: *MDParser, keep: u32) !void { _ = this; while (this.containers.len > keep) { var c = this.containers.last().?; var is_ordered_list = false; switch (c.ch) { ')', '.' => { is_ordered_list = true; }, '-', '+', '*' => { try this.pushContainerBytes( Block.Tag.li, c.task_mark_off, if (c.is_task) this.charAt(c.task_mark_off) else 0, Block.Flags.container_closer, ); try this.pushContainerBytes( if (is_ordered_list) Block.Tag.ol else Block.Tag.ul, c.ch, if (c.is_task) this.charAt(c.task_mark_off) else 0, Block.Flags.container_closer, ); }, '>' => { try this.pushContainerBytes( Block.Tag.quote, 0, 0, Block.Flags.container_closer, ); }, else => unreachable, } this.containers.len -= 1; } } fn enterChildContainers(this: *MDParser, keep: u32) !void { _ = this; var i: u32 = this.containers.len - keep; while (i < this.containers.len) : (i += 1) { var c: *Container = this.containers.ptr[i]; var is_ordered_list = false; switch (c.ch) { ')', '.' => { is_ordered_list = true; }, '-', '+', '*' => { // Remember offset in ctx.block_bytes so we can revisit the // block if we detect it is a loose list. try this.endCurrentBlock(); c.block_index = this.blocks.len; try this.pushContainerBytes( if (is_ordered_list) Block.Tag.ol else Block.Tag.ul, c.start, c.ch, Block.Flags.container_opener, ); try this.pushContainerBytes( Block.Tag.li, c.task_mark_off, if (c.is_task) this.charAt(c.task_mark_off) else 0, Block.Flags.container_opener, ); }, '>' => { try this.pushContainerBytes( Block.Tag.quote, 0, 0, Block.Flags.container_opener, ); }, else => unreachable, } } } fn pushContainer(this: *MDParser, container: Container) !void { try this.containers.push(this.allocator, container); } const LeafBlockDetail = union { none: void, h: Block.Header, code: Block.Code, table: Block.Table, }; fn processLeafBlockWithType(this: *MDParser, comptime tag: Block.Tag, block: *Block) anyerror!void { const BlockDetailType = comptime switch (tag) { Block.Tag.h => Block.Header, Block.Tag.code => Block.Code, Block.Tag.table => Block.Table, else => void, }; const is_in_tight_list = if (this.containers.len == 0) false else !this.containers.ptr[this.containers.len - 1].is_loose; const detail: BlockDetailType = switch (comptime tag) { Block.Tag.h => @truncate(Block.Header, block.data), Block.Tag.code => try this.setupFencedCodeDetail(block), Block.Tag.table => .{ .col_count = block.data, .head_row_count = 1, .body_row_count = block.line_count -| 2, }, else => void{}, }; if (!is_in_tight_list or comptime tag != .p) { try this.mdx.onEnterBlock(block.tag, BlockDetailType, detail); } defer { if (comptime tag == Block.Tag.code) {} } } fn processLeafBlock(this: *MDParser, block: *Block) anyerror!void { return switch (block.tag) { .doc => try this.processLeafBlockWithType(Block.Tag.doc, block), .quote => try this.processLeafBlockWithType(Block.Tag.quote, block), .ul => try this.processLeafBlockWithType(Block.Tag.ul, block), .ol => try this.processLeafBlockWithType(Block.Tag.ol, block), .li => try this.processLeafBlockWithType(Block.Tag.li, block), .hr => try this.processLeafBlockWithType(Block.Tag.hr, block), .h => try this.processLeafBlockWithType(Block.Tag.h, block), .code => try this.processLeafBlockWithType(Block.Tag.code, block), .html => try this.processLeafBlockWithType(Block.Tag.html, block), .p => try this.processLeafBlockWithType(Block.Tag.p, block), .table => try this.processLeafBlockWithType(Block.Tag.table, block), .thead => try this.processLeafBlockWithType(Block.Tag.thead, block), .tbody => try this.processLeafBlockWithType(Block.Tag.tbody, block), .tr => try this.processLeafBlockWithType(Block.Tag.tr, block), .th => try this.processLeafBlockWithType(Block.Tag.th, block), .td => try this.processLeafBlockWithType(Block.Tag.td, block), }; } fn pushContainerBytes(this: *MDParser, block_type: Block.Tag, start: u32, data: u32, flag: Block.Flags) !void { try this.endCurrentBlock(); var block = Block{ .tag = block_type, .line_count = start, .data = data, }; block.flags.insert(flag); var prev_block: ?Block = null; if (this.current_block) |curr| { prev_block = curr.*; } try this.blocks.push(this.allocator, block); if (prev_block != null) { this.current_block = this.blocks.ptr[this.current_block_index]; } } fn processAllBlocks(this: *MDParser) !void { _ = this; // ctx->containers now is not needed for detection of lists and list items // so we reuse it for tracking what lists are loose or tight. We rely // on the fact the vector is large enough to hold the deepest nesting // level of lists. this.containers.len = 0; var blocks = this.blocks.slice(); for (blocks) |*block| { const detail: Block.Detail = switch (block.tag) { .ul => Block.Detail{ .ul = .{ .is_tight = !block.flags.contains(.loose_list), .mark = @truncate(u8, block.data), }, }, .ol => Block.Detail{ .ol = .{ .start = block.line_count, .is_tight = !block.flags.contains(.loose_list), .mark_delimiter = @truncate(u8, block.data), }, }, .li => Block.Detail{ .li = .{ .is_task = block.data != 0, .task_mark = @truncate(u8, block.data), .task_mark_offset = @intCast(u32, block.line_count), }, }, else => Block.Detail{ .none = .{} }, }; if (block.flags.contains(.container)) { if (block.flags.contains(.container_closer)) { switch (block.tag) { .li => try this.mdx.onLeaveBlock(block.tag, Block.LI, detail.li), .ul => try this.mdx.onLeaveBlock(block.tag, Block.UL, detail.ul), .ol => try this.mdx.onLeaveBlock(block.tag, Block.OL, detail.ol), else => try this.mdx.onLeaveBlock(block.tag, void, void{}), } this.containers.len -|= switch (block.tag) { .ul, .ol, .blockquote => 1, else => 0, }; } if (block.flags.contains(.container_opener)) { switch (block.tag) { .li => try this.mdx.onEnterBlock(block.tag, Block.LI, detail.li), .ul => try this.mdx.onEnterBlock(block.tag, Block.UL, detail.ul), .ol => try this.mdx.onEnterBlock(block.tag, Block.OL, detail.ol), else => try this.mdx.onEnterBlock(block.tag, void, void{}), } switch (block.tag) { .ul, .ol => { this.containers.ptr[this.containers.len].is_loose = block.flags.contains(.loose_list); this.containers.len += 1; }, .blockquote => { // This causes that any text in a block quote, even if // nested inside a tight list item, is wrapped with //

    ...

    . */ this.containers.ptr[this.containers.len].is_loose = true; this.containers.len += 1; }, else => {}, } } } else { try this.processLeafBlock(block); } } } fn isContainerCompatible(pivot: *const Container, container: *const Container) bool { // Block quote has no "items" like lists. if (container.ch == '>') return false; if (container.ch != pivot.ch) return false; if (container.mark_indent > pivot.contents_indent) return false; return true; } fn isHRLine(this: *MDParser, beg: u32, end: *u32, hr_killer: *u32) bool { var off = beg + 1; var n: u32 = 1; while (off < this.size and (this.charAt(off) == this.charAt(beg) or this.charAt(off) == ' ' or this.charAt(off) == '\t')) { if (this.charAt(off) == this.charAt(beg)) n += 1; off += 1; } if (n < 3) { hr_killer.* = off; return false; } // Nothing else can be present on the line. */ if (off < this.size and !this.isNewline(off)) { hr_killer.* = off; return false; } end.* = off; return true; } fn isSetextUnderline(this: *MDParser, beg: u32, end: *u32, level: *u4) bool { var off = beg + 1; while (off < this.size and this.charAt(off) == this.charAt(beg)) off += 1; // Optionally, space(s) can follow. */ while (off < this.size and this.charAt(off) == ' ') off += 1; // But nothing more is allowed on the line. if (off < this.size and !this.isNewline(off)) return false; level.* = if (this.charAt(beg) == '=') 1 else 2; end.* = off; return true; } fn isATXHeaderLine(this: *MDParser, beg: u32, p_beg: *u32, end: *u32, level: *u4) bool { var n: i32 = undefined; var off: u32 = beg + 1; while (off < this.size and this.charAt(off) == '#' and off - beg < 7) { off += 1; } n = off - beg; if (n > 6) return false; level.* = @intCast(u4, n); if (!(this.flags.contains(.permissive_atxheaders)) and off < this.size and this.charAt(off) != ' ' and this.charAt(off) != '\t' and !this.isNewline(off)) return false; while (off < this.size and this.charAt(off) == ' ') { off += 1; } p_beg.* = off; end.* = off; return true; } fn isTableUnderline(this: *MDParser, beg: u32, end: *u32, column_column: *u32) bool { _ = this; _ = end; _ = column_column; var off = beg; var found_pipe = false; var col_count: u32 = 0; if (off < this.size and this.charAt(off) == '|') { found_pipe = true; off += 1; while (off < this.size and isWhitespace(this.charAt(off))) { off += 1; } } while (true) { var delimited = false; // Cell underline ("-----", ":----", "----:" or ":----:")if(off < this.size and this.charAt(off) == _T(':')) off += 1; if (off >= this.size or this.charAt(off) != '-') return false; while (off < this.size and this.charAt(off) == '-') off += 1; if (off < this.size and this.charAt(off) == ':') off += 1; col_count += 1; // Pipe delimiter (optional at the end of line). */ while (off < this.size and isWhitespace(this.charAt(off))) off += 1; if (off < this.size and this.charAt(off) == '|') { delimited = true; found_pipe = true; off += 1; while (off < this.size and isWhitespace(this.charAt(off))) off += 1; } // Success, if we reach end of line. if (off >= this.size or this.isNewline(off)) break; if (!delimited) return false; } if (!found_pipe) return false; column_column.* = col_count; end.* = off; return true; } fn isOpeningCodeFence(this: *MDParser, beg: u8, end: *u32) bool { var off = beg; const first = this.charAt(beg); while (off < this.size and this.charAt(off) == first) { off += 1; } // Fence must have at least three characters. if (off - beg < 3) return false; // Optionally, space(s) can follow while (off < this.size and this.charAt(off) == ' ') { off += 1; } // Optionally, an info string can follow. while (off < this.size and !this.isNewline(this.charAt(off))) { // Backtick-based fence must not contain '`' in the info string. if (first == '`' and this.charAt(off) == '`') return false; off += 1; } end.* = off; return true; } fn isClosingCodeFence(this: *MDParser, ch: u8, beg: u8, end: *u32) bool { var off = beg; defer { end.* = off; } while (off < this.size and this.charAt(off) == ch) { off += 1; } if (off - beg < this.code_fence_length) { return false; } // Optionally, space(s) can follow while (off < this.size and this.charAt(off) == ' ') { off += 1; } // But nothing more is allowed on the line. if (off < this.size and !this.isNewline(this.charAt(off))) return false; return true; } pub fn parse(this: *MDParser) anyerror!void { var pivot_line = &Line.Analysis.blank; var line_buf: [2]Line.Analysis = undefined; var line = &line_buf[0]; var offset: u32 = 0; try this.mdx.onEnterBlock(.doc, void, void{}); const len: u32 = this.size; while (offset < len) { if (line == pivot_line) { line = if (line == &line_buf[0]) &line_buf[1] else &line_buf[0]; } try this.analyzeLine(offset, &offset, pivot_line, line); try this.processLine(&pivot_line, line); } this.endCurrentBlock(); try this.buildRefDefHashTable(); this.leaveChildContainers(0); this.processAllBlocks(); try this.mdx.onLeaveBlock(.doc, void, void{}); } }; pub const MDX = struct { parser: JSParser, log: *logger.Log, allocator: std.mem.Allocator, stmts: std.ArrayListUnmanaged(js_ast.Stmt) = .{}, pub const Options = struct {}; pub fn onEnterBlock(this: *MDX, tag: Block.Tag, comptime Detail: type, detail: Detail) anyerror!void { _ = tag; _ = detail; _ = this; } pub fn onLeaveBlock(this: *MDX, tag: Block.Tag, comptime Detail: type, detail: Detail) anyerror!void { _ = tag; _ = detail; _ = this; } pub fn onEnterSpan(this: *MDX, tag: Span.Tag, comptime Detail: type, detail: Detail) anyerror!void { _ = tag; _ = detail; _ = this; } pub fn onLeaveSpan(this: *MDX, tag: Span.Tag, comptime Detail: type, detail: Detail) anyerror!void { _ = tag; _ = detail; _ = this; } pub fn onText(this: *MDX, tag: Text, text: []const u8) anyerror!void { _ = tag; _ = text; _ = this; } pub inline fn source(p: *const MDX) *const logger.Source { return &p.lexer.source; } pub fn e(_: *MDX, t: anytype, loc: logger.Loc) Expr { const Type = @TypeOf(t); if (@typeInfo(Type) == .Pointer) { return Expr.init(std.meta.Child(Type), t.*, loc); } else { return Expr.init(Type, t, loc); } } pub fn s(_: *MDX, t: anytype, loc: logger.Loc) Stmt { const Type = @TypeOf(t); if (@typeInfo(Type) == .Pointer) { return Stmt.init(std.meta.Child(Type), t.*, loc); } else { return Stmt.alloc(Type, t, loc); } } pub fn setup( this: *MDX, _options: ParserOptions, log: *logger.Log, source_: *const logger.Source, define: *Define, allocator: std.mem.Allocator, ) !void { try JSParser.init( allocator, log, source_, define, js_lexer.Lexer.initNoAutoStep(log, source_.*, allocator), _options, &this.parser, ); this.lexer = try Lexer.init(&this.parser.lexer); this.allocator = allocator; this.log = log; this.stmts = .{}; } pub fn parse(this: *MDX) !js_ast.Result { try this._parse(); return try runVisitPassAndFinish(JSParser, &this.parser, this.stmts.toOwnedSlice(this.allocator)); } fn run(this: *MDX) anyerror!logger.Loc { _ = this; return logger.Loc.Empty; } fn _parse(this: *MDX) anyerror!void { var root_children = std.ArrayListUnmanaged(Expr){}; var first_loc = try run(this, &root_children); first_loc.start = @maximum(first_loc.start, 0); const args_loc = first_loc; first_loc.start += 1; const body_loc = first_loc; // We need to simulate a function that was parsed _ = try this.parser.pushScopeForParsePass(.function_args, args_loc); _ = try this.parser.pushScopeForParsePass(.function_body, body_loc); const root = this.e(E.JSXElement{ .tag = this.e(E.JSXElement.Tag.map.get(E.JSXElement.Tag.main), body_loc), .children = ExprNodeList.fromList(root_children), }, body_loc); var root_stmts = try this.allocator.alloc(Stmt, 1); root_stmts[0] = this.s(S.Return{ .value = root }, body_loc); try this.stmts.append( this.allocator, this.s(S.ExportDefault{ .default_name = try this.parser.createDefaultName(args_loc), .value = .{ .expr = this.e(E.Arrow{ .body = G.FnBody{ .stmts = root_stmts, .loc = body_loc, }, .args = &[_]G.Arg{}, .prefer_expr = true, }, args_loc), }, }, args_loc), ); } };