package d2parser

import (
	
	
	
	
	
	
	
	
	

	tunicode 
	

	
	
)

type ParseOptions struct {
	// UTF16Pos would be used with input received from a browser where the browser will send the text as UTF-8 but
	// JavaScript keeps strings in memory as UTF-16 and so needs UTF-16 indexes into the text to line up errors correctly.
	// So you want to read UTF-8 still but adjust the indexes to pretend the input is utf16.
	UTF16Pos bool

	ParseError *ParseError
}

// Parse parses a .d2 Map in r.
//
// The returned Map always represents a valid .d2 file. All encountered errors will be in
// []error.
//
// The map may be compiled via Compile even if there are errors to keep language tooling
// operational. Though autoformat should not run.
//
// If UTF16Pos is true, positions will be recorded in UTF-16 codeunits as required by LSP
// and browser clients. See
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocuments
// TODO: update godocs
func ( string,  io.Reader,  *ParseOptions) (*d2ast.Map, error) {
	if  == nil {
		 = &ParseOptions{
			UTF16Pos: false,
		}
	}

	 := &parser{
		path: ,

		utf16Pos: .UTF16Pos,
		err:      .ParseError,
	}
	 := bufio.NewReader()
	.reader = 

	,  := .Peek(2)
	if  == nil {
		// 0xFFFE is invalid UTF-8 so this is safe.
		// Also a different BOM is used for UTF-8.
		// See https://unicode.org/faq/utf_bom.html#bom4
		if [0] == 0xFF && [1] == 0xFE {
			.utf16Pos = true

			 := make([]byte, .Buffered())
			io.ReadFull(, )

			 := io.MultiReader(bytes.NewBuffer(), )
			 := transform.NewReader(, tunicode.UTF16(tunicode.LittleEndian, tunicode.UseBOM).NewDecoder())
			.Reset()
		}
	}

	if .err == nil {
		.err = &ParseError{}
	}

	 := .parseMap(true)
	if !.err.Empty() {
		return , .err
	}
	return , nil
}

func ( string) (*d2ast.KeyPath, error) {
	 := &parser{
		reader: strings.NewReader(),
		err:    &ParseError{},
	}

	 := .parseKey()
	if !.err.Empty() {
		return nil, fmt.Errorf("failed to parse key %q: %w", , .err)
	}
	if  == nil {
		return nil, fmt.Errorf("empty key: %q", )
	}
	return , nil
}

func ( string) (*d2ast.Key, error) {
	 := &parser{
		reader: strings.NewReader(),
		err:    &ParseError{},
	}

	 := .parseMapKey()
	if !.err.Empty() {
		return nil, fmt.Errorf("failed to parse map key %q: %w", , .err)
	}
	if  == nil {
		return nil, fmt.Errorf("empty map key: %q", )
	}
	return , nil
}

func ( string) (d2ast.Value, error) {
	 := &parser{
		reader: strings.NewReader(),
		err:    &ParseError{},
	}

	 := .parseValue()
	if !.err.Empty() {
		return nil, fmt.Errorf("failed to parse value %q: %w", , .err)
	}
	if .Unbox() == nil {
		return nil, fmt.Errorf("empty value: %q", )
	}
	return .Unbox(), nil
}

// TODO: refactor parser to keep entire file in memory as []rune
//   - trivial to then convert positions
//   - lookahead is gone, just forward back as much as you want :)
//   - streaming parser isn't really helpful.
//   - just read into a string even and decode runes forward/back as needed
//   - the whole file essentially exists within the parser as the AST anyway...
//
// TODO: ast struct that combines map & errors and pass that around
type parser struct {
	path     string
	pos      d2ast.Position
	utf16Pos bool

	reader    io.RuneReader
	readerPos d2ast.Position

	readahead    []rune
	lookahead    []rune
	lookaheadPos d2ast.Position

	ioerr bool
	err   *ParseError

	inEdgeGroup bool

	depth int
}

// TODO: rename to Error and make existing Error a private type errorWithRange
type ParseError struct {
	// Errors from globs need to be deduplicated
	ErrorsLookup map[d2ast.Error]struct{} `json:"-"`
	Errors       []d2ast.Error            `json:"errs"`
}

func ( d2ast.Node,  string,  ...interface{}) error {
	 = "%v: " + 
	 = append([]interface{}{.GetRange()}, ...)
	return d2ast.Error{
		Range:   .GetRange(),
		Message: fmt.Sprintf(, ...),
	}
}

func ( *ParseError) () bool {
	if  == nil {
		return true
	}
	return len(.Errors) == 0
}

func ( *ParseError) () string {
	var  strings.Builder
	for ,  := range .Errors {
		if  > 0 {
			.WriteByte('\n')
		}
		.WriteString(.Error())
	}
	return .String()
}

func ( *parser) ( d2ast.Position,  d2ast.Position,  string,  ...interface{}) {
	 := d2ast.Range{
		Path:  .path,
		Start: ,
		End:   ,
	}
	 = "%v: " + 
	 = append([]interface{}{}, ...)
	.err.Errors = append(.err.Errors, d2ast.Error{
		Range:   ,
		Message: fmt.Sprintf(, ...),
	})
}

// _readRune reads the next rune from the underlying reader or from the p.readahead buffer.
func ( *parser) () ( rune,  bool) {
	if len(.readahead) > 0 {
		 = .readahead[0]
		.readahead = append(.readahead[:0], .readahead[1:]...)
		return , false
	}

	if .ioerr {
		.rewind()
		return 0, true
	}

	.readerPos = .lookaheadPos

	, ,  := .reader.ReadRune()
	if  != nil {
		.ioerr = true
		if  != io.EOF {
			.err.Errors = append(.err.Errors, d2ast.Error{
				Range: d2ast.Range{
					Path:  .path,
					Start: .readerPos,
					End:   .readerPos,
				},
				Message: fmt.Sprintf("io error: %v", ),
			})
		}
		.rewind()
		return 0, true
	}
	return , false
}

func ( *parser) () ( rune,  bool) {
	,  = ._readRune()
	if  {
		return 0, true
	}
	.pos = .pos.Advance(, .utf16Pos)
	.lookaheadPos = .pos
	return , false
}

func ( *parser) ( rune) {
	.pos = .pos.Subtract(, .utf16Pos)

	// This is more complex than it needs to be to allow reusing the buffer underlying
	// p.lookahead.
	 := len(.lookahead) + 1
	if  > cap(.lookahead) {
		 := make([]rune, )
		copy([1:], .lookahead)
		.lookahead = 
	} else {
		.lookahead = .lookahead[:]
		copy(.lookahead[1:], .lookahead)
	}
	.lookahead[0] = 

	.rewind()
}

// peek returns the next rune without advancing the parser.
// You *must* call commit or rewind afterwards.
func ( *parser) () ( rune,  bool) {
	,  = ._readRune()
	if  {
		return 0, true
	}

	.lookahead = append(.lookahead, )
	.lookaheadPos = .lookaheadPos.Advance(, .utf16Pos)
	return , false
}

// TODO: this can replace multiple peeks i think, just return []rune instead
func ( *parser) ( int) ( string,  bool) {
	var  strings.Builder
	for  := 0;  < ; ++ {
		,  := .peek()
		if  {
			return .String(), true
		}
		.WriteRune()
	}
	return .String(), false
}

func ( *parser) () ( rune,  bool) {
	for {
		,  = .read()
		if  {
			return 0, true
		}
		if unicode.IsSpace() {
			continue
		}
		return , false
	}
}

// peekNotSpace returns the next non space rune without advancing the parser.
//
// newline is set if the next non space character is on a different line
// than the current line.
//
// TODO: everywhere this is used, we support newline escapes and so can just
// add the logic here and it should *just* work
// except line comments iirc
// not entirely sure, maybe i can put it into peek somehow
func ( *parser) () ( rune,  int,  bool) {
	for {
		,  = .peek()
		if  {
			return 0, 0, true
		}
		if unicode.IsSpace() {
			if  == '\n' {
				++
			}
			continue
		}
		return , , false
	}
}

// commit advances p.pos by all peeked bytes and then resets the p.lookahead buffer.
func ( *parser) () {
	.pos = .lookaheadPos
	.lookahead = .lookahead[:0]
}

// rewind copies p.lookahead to the front of p.readahead and then resets the p.lookahead buffer.
// All peeked bytes will again be available via p.eat or p.peek.
// TODO:
// peek
// peekn
// peekNotSpace
// commit
// rewind
//
// TODO: make each parse function read its delimiter and return nil if not as expected
// TODO: lookahead *must* always be empty in between parse calls. you either commit or
//
//	rewind in each function. if you don't, you pass a hint.
//
// TODO: omg we don't need two buffers, just a single lookahead and an index...
// TODO: get rid of lookaheadPos or at least never use directly. maybe rename to beforePeekPos?
//
//	or better yet keep positions in the lookahead buffer.
//	ok so plan here is to get rid of lookaheadPos and add a rewindPos that stores
//	the pos to rewind to.
func ( *parser) () {
	if len(.lookahead) == 0 {
		return
	}

	// This is more complex than it needs to be to allow reusing the buffer underlying
	// p.readahead.
	 := len(.lookahead) + len(.readahead)
	if cap(.readahead) <  {
		 := make([]rune, )
		copy([len(.lookahead):], .readahead)
		.readahead = 
	} else {
		.readahead = .readahead[:]
		copy(.readahead[len(.lookahead):], .readahead)
	}
	copy(.readahead, .lookahead)

	.lookahead = .lookahead[:0]
	.lookaheadPos = .pos
}

// TODO: remove isFileMap like in printer. can't rn as we have to subtract delim
func ( *parser) ( bool) *d2ast.Map {
	 := &d2ast.Map{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos,
		},
	}
	defer .Range.End.From(&.pos)

	if ! {
		.Range.Start = .Range.Start.Subtract('{', .utf16Pos)
		.depth++
		defer dec(&.depth)
	}

	for {
		,  := .readNotSpace()
		if  {
			if ! {
				.errorf(.Range.Start, .readerPos, "maps must be terminated with }")
			}
			return 
		}

		switch  {
		case ';':
			continue
		case '}':
			if  {
				.errorf(.pos.Subtract(, .utf16Pos), .pos, "unexpected map termination character } in file map")
				continue
			}
			return 
		}

		 := .parseMapNode()
		if .Unbox() != nil {
			.Nodes = append(.Nodes, )
			// TODO: all subsequent not comment characters on the current line (or till ;)
			// need to be considered errors.
			// TODO: add specific msg for each bad rune type
		}

		if .BlockComment != nil {
			// Anything after a block comment is ok.
			continue
		}

		 := .pos
		for {
			, ,  := .peekNotSpace()
			if  ||  != 0 ||  == ';' ||  == '}' ||  == '#' {
				.rewind()
				break
			}
			.commit()
		}

		// TODO: maybe better idea here is to make parseUnquotedString aware of its delimiters
		// better and so it would read technically invalid characters and just complain.
		// TODO: that way broken syntax will be parsed more "intently". would work better with
		// language tooling I think though not sure. yes definitely, eaterr!
		if  != .pos {
			if .Unbox() != nil {
				if .MapKey != nil && .MapKey.Value.Unbox() != nil {
					 := ""
					if ,  := .MapKey.Value.Unbox().(*d2ast.BlockString);  {
						 = ". See https://d2lang.com/tour/text#advanced-block-strings."
					}
					.errorf(, .pos, "unexpected text after %v%s", .MapKey.Value.Unbox().Type(), )
				} else {
					.errorf(, .pos, "unexpected text after %v", .Unbox().Type())
				}
			} else {
				.errorf(, .pos, "invalid text beginning unquoted key")
			}
		}
	}
}

func ( *parser) ( rune) d2ast.MapNodeBox {
	var  d2ast.MapNodeBox

	switch  {
	case '#':
		.Comment = .parseComment()
		return 
	case '"':
		,  := .peekn(2)
		if  {
			break
		}
		if  != `""` {
			.rewind()
			break
		}
		.commit()
		.BlockComment = .parseBlockComment()
		return 
	case '.':
		,  := .peekn(2)
		if  {
			break
		}
		if  != ".." {
			.rewind()
			break
		}
		,  := .peek()
		if  {
			break
		}
		if  == '$' {
			.commit()
			.Substitution = .parseSubstitution(true)
			return 
		}
		if  == '@' {
			.commit()
			.Import = .parseImport(true)
			return 
		}
		.rewind()
		break
	}

	.replay()
	.MapKey = .parseMapKey()
	return 
}

func ( *parser) () *d2ast.Comment {
	 := &d2ast.Comment{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('#', .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	var  strings.Builder
	defer func() {
		.Value = .String()
	}()
	.parseCommentLine(, &)

	for {
		, ,  := .peekNotSpace()
		if  {
			return 
		}
		if  != '#' ||  >= 2 {
			.rewind()
			return 
		}
		.commit()

		if  == 1 {
			.WriteByte('\n')
		}

		.parseCommentLine(, &)
	}
}

func ( *parser) ( *d2ast.Comment,  *strings.Builder) {
	 := true
	for {
		,  := .peek()
		if  {
			return
		}
		if  == '\n' {
			.rewind()
			return
		}
		.commit()

		if  {
			 = false
			if  == ' ' {
				continue
			}
		}
		.WriteRune()
	}
}

func ( *parser) () *d2ast.BlockComment {
	 := &d2ast.BlockComment{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.SubtractString(`"""`, .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	.depth++
	defer dec(&.depth)

	var  strings.Builder
	defer func() {
		.Value = trimSpaceAfterLastNewline(.String())
		.Value = trimCommonIndent(.Value)
	}()

	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `block comments must be terminated with """`)
			return 
		}

		if !unicode.IsSpace() {
			.rewind()
			break
		}
		.commit()
		if  == '\n' {
			break
		}
	}

	for {
		,  := .read()
		if  {
			.errorf(.Range.Start, .readerPos, `block comments must be terminated with """`)
			return 
		}

		if  != '"' {
			.WriteRune()
			continue
		}

		,  := .peekn(2)
		if  {
			.errorf(.Range.Start, .readerPos, `block comments must be terminated with """`)
			return 
		}
		if  != `""` {
			.WriteByte('"')
			.rewind()
			continue
		}
		.commit()
		return 
	}
}

func trimSpaceAfterLastNewline( string) string {
	 := strings.LastIndexByte(, '\n')
	if  == -1 {
		return strings.TrimRightFunc(, unicode.IsSpace)
	}

	 := [+1:]
	 = strings.TrimRightFunc(, unicode.IsSpace)
	if len() == 0 {
		return [:]
	}
	return [:+1] + 
}

func ( *parser) () ( *d2ast.Key) {
	 = &d2ast.Key{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos,
		},
	}
	defer .Range.End.From(&.pos)

	defer func() {
		if .Key == nil && len(.Edges) == 0 {
			 = nil
		}
	}()

	// Check for not ampersand/@.
	,  := .peek()
	if  {
		return 
	}
	if  == '!' {
		,  := .peek()
		if  {
			return 
		}
		if  == '&' {
			.commit()
			.NotAmpersand = true
		} else {
			.rewind()
		}
	} else if  == '&' {
		.commit()
		.Ampersand = true
	} else {
		.rewind()
	}

	,  = .peek()
	if  {
		return 
	}
	if  == '(' {
		.commit()
		.parseEdgeGroup()
		return 
	}
	.rewind()

	 := .parseKey()
	if  != nil {
		.Key = 
	}

	, ,  := .peekNotSpace()
	if  {
		return 
	}
	if  > 0 {
		.rewind()
		return 
	}
	switch  {
	case '(':
		.commit()
		.parseEdgeGroup()
		return 
	case '<', '>', '-':
		.rewind()
		.Key = nil
		.parseEdges(, )
		.parseMapKeyValue()
		return 
	default:
		.rewind()
		.parseMapKeyValue()
		return 
	}
}

func ( *parser) ( *d2ast.Key) {
	, ,  := .peekNotSpace()
	if  {
		return
	}
	if  > 0 {
		.rewind()
		return
	}

	switch  {
	case '{':
		.rewind()
		if .Key == nil && len(.Edges) == 0 {
			return
		}
	case ':':
		.commit()
		if .Key == nil && len(.Edges) == 0 {
			.errorf(.Range.Start, .pos, "map value without key")
		}
	default:
		.rewind()
		return
	}
	.Value = .parseValue()
	if .Value.Unbox() == nil {
		.errorf(.pos.Subtract(':', .utf16Pos), .pos, "missing value after colon")
	}

	 := .Value.ScalarBox()
	// If the value is a scalar, then check if it's the primary value.
	if .Unbox() != nil {
		, ,  := .peekNotSpace()
		if  ||  > 0 ||  != '{' {
			.rewind()
			return
		}
		// Next character is on the same line without ; separator so it must mean
		// our current value is the Primary and the next is the Value.
		.commit()
		.replay()
		.Primary = 
		.Value = .parseValue()
	}
}

func ( *parser) ( *d2ast.Key) {
	// To prevent p.parseUnquotedString from consuming terminating parentheses.
	.inEdgeGroup = true
	defer func() {
		.inEdgeGroup = false
	}()

	 := .parseKey()
	.parseEdges(, )

	, ,  := .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return
	}
	if  != ')' {
		.rewind()
		.errorf(.Range.Start, .pos, "edge groups must be terminated with )")
		return
	}
	.commit()

	, ,  = .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return
	}
	if  == '[' {
		.commit()
		.EdgeIndex = .parseEdgeIndex()
	} else {
		.rewind()
	}

	, ,  = .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return
	}
	if  == '.' {
		.commit()
		.EdgeKey = .parseKey()
	} else {
		.rewind()
	}

	.inEdgeGroup = false
	.parseMapKeyValue()
}

func ( *parser) () *d2ast.EdgeIndex {
	 := &d2ast.EdgeIndex{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('[', .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	, ,  := .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return nil
	}

	if unicode.IsDigit() {
		.commit()
		var  strings.Builder
		.WriteRune()
		for {
			, ,  = .peekNotSpace()
			if  ||  > 0 {
				.rewind()
				.errorf(.Range.Start, .pos, "unterminated edge index")
				return nil
			}
			if  == ']' {
				.rewind()
				break
			}
			.commit()
			if !unicode.IsDigit() {
				.errorf(.pos.Subtract(, .utf16Pos), .pos, "unexpected character in edge index")
				continue
			}
			.WriteRune()
		}
		,  := strconv.Atoi(.String())
		.Int = &
	} else if  == '*' {
		.commit()
		.Glob = true
	} else {
		.errorf(.pos.Subtract(, .utf16Pos), .pos, "unexpected character in edge index")
		// TODO: skip to ], maybe add a p.skipTo to skip to certain characters
	}

	, ,  = .peekNotSpace()
	if  ||  > 0 ||  != ']' {
		.rewind()
		.errorf(.Range.Start, .pos, "unterminated edge index")
		return 
	}
	.commit()
	return 
}

func ( *parser) ( *d2ast.Key,  *d2ast.KeyPath) {
	for {
		 := &d2ast.Edge{
			Range: d2ast.Range{
				Path: .path,
			},
			Src: ,
		}
		if  != nil {
			.Range.Start = .Range.Start
		} else {
			.Range.Start = .pos
		}

		, ,  := .peekNotSpace()
		if  {
			return
		}
		if  > 0 {
			.rewind()
			return
		}
		if  == '<' ||  == '*' {
			.SrcArrow = string()
		} else if  != '-' {
			.rewind()
			return
		}
		if  == nil {
			.errorf(.lookaheadPos.Subtract(, .utf16Pos), .lookaheadPos, "connection missing source")
			.Range.Start = .lookaheadPos.Subtract(, .utf16Pos)
		}
		.commit()

		if !.parseEdge() {
			return
		}

		 := .parseKey()
		if  == nil {
			.errorf(.Range.Start, .pos, "connection missing destination")
		} else {
			.Dst = 
			.Range.End = .Dst.Range.End
		}
		.Edges = append(.Edges, )
		 = 
	}
}

func ( *parser) ( *d2ast.Edge) ( bool) {
	defer .Range.End.From(&.pos)

	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, "unterminated connection")
			return false
		}
		switch  {
		case '>', '*':
			.DstArrow = string()
			.commit()
			return true
		case '\\':
			.commit()
			, ,  := .peekNotSpace()
			if  {
				continue
			}
			if  == 0 {
				.rewind()
				.errorf(.Range.Start, .readerPos, "only newline escapes are allowed in connections")
				return false
			}
			if  > 1 {
				.rewind()
				continue
			}
			.commit()
			.replay()
		case '-':
			.commit()
		default:
			.rewind()
			return true
		}
	}
}

func ( *parser) () ( *d2ast.KeyPath) {
	 = &d2ast.KeyPath{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos,
		},
	}

	defer func() {
		if len(.Path) == 0 {
			 = nil
		} else {
			.Range.End = .Path[len(.Path)-1].Unbox().GetRange().End
		}
	}()

	for {
		, ,  := .peekNotSpace()
		if  {
			return 
		}
		if  > 0 ||  == '(' {
			.rewind()
			return 
		}
		// TODO: error if begin, but see below too
		if  == '.' {
			continue
		}
		.rewind()

		 := .parseString(true)
		 := .Unbox()
		if  == nil {
			return 
		}
		if .UnquotedString != nil && strings.HasPrefix(.ScalarString(), "@") {
			.errorf(.GetRange().Start, .GetRange().End, "%s is not a valid import, did you mean ...%[2]s?", .ScalarString())
		}

		if len(.Path) == 0 {
			.Range.Start = .GetRange().Start
		}
		.Path = append(.Path, &)

		, ,  = .peekNotSpace()
		if  {
			return 
		}
		if  > 0 ||  != '.' {
			.rewind()
			return 
		}
		// TODO: error if not string or ( after, see above too
		.commit()
	}
}

// TODO: inKey -> p.inKey (means I have to restore though)
func ( *parser) ( bool) d2ast.StringBox {
	var  d2ast.StringBox

	, ,  := .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return 
	}
	.commit()

	switch  {
	case '"':
		.DoubleQuotedString = .parseDoubleQuotedString()
		return 
	case '\'':
		.SingleQuotedString = .parseSingleQuotedString()
		return 
	case '|':
		.BlockString = .parseBlockString()
		return 
	default:
		.replay()
		.UnquotedString = .parseUnquotedString()
		return 
	}
}

func ( *parser) ( bool) ( *d2ast.UnquotedString) {
	 = &d2ast.UnquotedString{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos,
		},
	}
	// TODO: fix unquoted end whitespace handling to peekNotSpace
	 := .pos
	defer .Range.End.From(&)

	var  strings.Builder
	var  strings.Builder
	 := 0
	defer func() {
		 := strings.TrimRightFunc(.String(), unicode.IsSpace)
		 := strings.TrimRightFunc(.String(), unicode.IsSpace)
		if .Pattern != nil {
			if  < len() {
				.Pattern = append(.Pattern, [:])
			}
		}
		if  == "" {
			if len(.Value) > 0 {
				return
			}
			 = nil
			// TODO: this should be in the parent and instead they check the delimiters first
			// 			 or last really. only in parseMapNode && parseArrayNode
			// TODO: give specific descriptions for each kind of special character that could have caused this.
			return
		}
		.Value = append(.Value, d2ast.InterpolationBox{String: &, StringRaw: &})
	}()

	,  := .peekn(4)
	.rewind()
	if ! {
		if  == "...@" {
			.errorf(.pos, .pos.AdvanceString("...@", .utf16Pos), "unquoted strings cannot begin with ...@ as that's import spread syntax")
		}
	}

	for {
		,  := .peek()
		if  {
			return 
		}

		if .inEdgeGroup &&  == ')' {
			// TODO: need a peekNotSpace across escaped newlines
			, ,  := .peekNotSpace()
			if  ||  > 0 {
				.rewind()
				return 
			}
			switch  {
			case '\n', '#', '{', '}', '[', ']', ':', '.':
				.rewind()
				return 
			}
			.rewind()
			.peek()
			.commit()
			 = .pos
			.WriteRune()
			.WriteRune()
			continue
		}

		// top:   '\n', '#', '{', '}', '[', ']'
		// keys:  ':', '.'
		// edges: '<', '>', '(', ')',
		// edges: --, ->, -*, *-
		switch  {
		case '\n', ';', '#', '{', '}', '[', ']':
			.rewind()
			return 
		}
		if  {
			switch  {
			case ':', '.', '<', '>', '&':
				.rewind()
				return 
			case '-':
				// TODO: need a peekNotSpace across escaped newlines
				,  := .peek()
				if  {
					return 
				}
				switch  {
				case '\n', ';', '#', '{', '}', '[', ']':
					.rewind()
					.peek()
					.commit()
					.WriteRune()
					.WriteRune()
					return 
				}
				if  == '-' ||  == '>' ||  == '*' {
					.rewind()
					return 
				}
				.WriteRune()
				.WriteRune()
				 = 
			}
		}

		if  == '*' {
			if .Len() == 0 {
				.Pattern = append(.Pattern, "*")
			} else {
				.Pattern = append(.Pattern, .String()[:], "*")
			}
			 = len(.String()) + 1
		}

		.commit()

		if !unicode.IsSpace() {
			 = .pos
		}

		if ! &&  == '$' {
			 := .parseSubstitution(false)
			if  != nil {
				if .Len() > 0 {
					 := .String()
					 := .String()
					.Value = append(.Value, d2ast.InterpolationBox{String: &, StringRaw: &})
					.Reset()
					.Reset()
				}
				.Value = append(.Value, d2ast.InterpolationBox{Substitution: })
				continue
			}
			continue
		}

		if  != '\\' {
			.WriteRune()
			.WriteRune()
			continue
		}

		,  := .read()
		if  {
			.errorf(.pos.Subtract('\\', .utf16Pos), .readerPos, "unfinished escape sequence")
			return 
		}

		if  == '\n' {
			, ,  := .peekNotSpace()
			if  ||  > 0 {
				.rewind()
				return 
			}
			.commit()
			.replay()
			continue
		}

		.WriteRune(decodeEscape())
		.WriteByte('\\')
		.WriteRune()
	}
}

// https://go.dev/ref/spec#Rune_literals
// TODO: implement all Go escapes like the unicode ones
func decodeEscape( rune) rune {
	switch  {
	case 'a':
		return '\a'
	case 'b':
		return '\b'
	case 'f':
		return '\f'
	case 'n':
		return '\n'
	case 'r':
		return '\r'
	case 't':
		return '\t'
	case 'v':
		return '\v'
	case '\\':
		return '\\'
	case '"':
		return '"'
	default:
		return 
	}
}

func ( *parser) ( bool) *d2ast.DoubleQuotedString {
	 := &d2ast.DoubleQuotedString{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('"', .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	var  strings.Builder
	var  strings.Builder
	defer func() {
		if .Len() > 0 {
			 := .String()
			 := .String()
			.Value = append(.Value, d2ast.InterpolationBox{String: &, StringRaw: &})
		}
	}()

	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `double quoted strings must be terminated with "`)
			return 
		}
		if  == '\n' {
			.rewind()
			.errorf(.Range.Start, .pos, `double quoted strings must be terminated with "`)
			return 
		}

		.commit()
		if ! &&  == '$' {
			 := .parseSubstitution(false)
			if  != nil {
				if .Len() > 0 {
					.Value = append(.Value, d2ast.InterpolationBox{String: go2.Pointer(.String())})
					.Reset()
				}
				.Value = append(.Value, d2ast.InterpolationBox{Substitution: })
				continue
			}
		}

		if  == '"' {
			return 
		}

		if  != '\\' {
			.WriteRune()
			.WriteRune()
			continue
		}

		,  := .read()
		if  {
			.errorf(.pos.Subtract('\\', .utf16Pos), .readerPos, "unfinished escape sequence")
			.errorf(.Range.Start, .readerPos, `double quoted strings must be terminated with "`)
			return 
		}

		if  == '\n' {
			// TODO: deindent
			continue
		}
		.WriteRune(decodeEscape())
		.WriteByte('\\')
		.WriteRune()
	}
}

func ( *parser) () *d2ast.SingleQuotedString {
	 := &d2ast.SingleQuotedString{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('\'', .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	var  strings.Builder
	defer func() {
		.Value = .String()
	}()

	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `single quoted strings must be terminated with '`)
			return 
		}
		if  == '\n' {
			.rewind()
			.errorf(.Range.Start, .pos, `single quoted strings must be terminated with '`)
			return 
		}
		.commit()

		if  == '\'' {
			,  = .peek()
			if  {
				return 
			}
			if  == '\'' {
				.commit()
				.WriteByte('\'')
				continue
			}
			.rewind()
			return 
		}

		if  != '\\' {
			.WriteRune()
			continue
		}

		,  := .peek()
		if  {
			continue
		}

		switch  {
		case '\n':
			.commit()
			continue
		default:
			.WriteRune()
			.rewind()
		}
	}
}

func ( *parser) () *d2ast.BlockString {
	 := &d2ast.BlockString{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('|', .utf16Pos),
		},
	}
	defer .Range.End.From(&.pos)

	.depth++
	defer dec(&.depth)

	var  strings.Builder
	defer func() {
		.Value = trimSpaceAfterLastNewline(.String())
		.Value = trimCommonIndent(.Value)
	}()

	// Do we have more symbol quotes?
	.Quote = ""
	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `block string must be terminated with %v`, .Quote+"|")
			return 
		}

		if unicode.IsSpace() || unicode.IsLetter() || unicode.IsDigit() ||  == '_' {
			.rewind()
			break
		}
		.commit()
		.Quote += string()
	}

	// Do we have a tag?
	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `block string must be terminated with %v`, .Quote+"|")
			return 
		}

		if unicode.IsSpace() {
			.rewind()
			break
		}
		.commit()
		.Tag += string()
	}
	if .Tag == "" {
		// TODO: no and fix compiler to not set text/markdown shape always.
		//       reason being not all multiline text is markdown by default.
		//       for example markdown edge labels or other random text.
		//       maybe we can be smart about this at some point and only set
		//       if the block string is being interpreted as markdown.
		.Tag = "md"
	}

	// Skip non newline whitespace.
	for {
		,  := .peek()
		if  {
			.errorf(.Range.Start, .readerPos, `block string must be terminated with %v`, .Quote+"|")
			return 
		}
		if !unicode.IsSpace() {
			// Non whitespace characters on the first line have an implicit indent.
			.WriteString(.getIndent())
			.rewind()
			break
		}
		.commit()
		if  == '\n' {
			break
		}
	}

	 := '|'
	 := ""
	if len(.Quote) > 0 {
		var  int
		,  = utf8.DecodeLastRuneInString(.Quote)
		 = .Quote[:] + "|"
	}

	for {
		,  := .read()
		if  {
			.errorf(.Range.Start, .readerPos, `block string must be terminated with %v`, .Quote+"|")
			return 
		}

		if  !=  {
			.WriteRune()
			continue
		}

		,  := .peekn(len())
		if  {
			.errorf(.Range.Start, .readerPos, `block string must be terminated with %v`, .Quote+"|")
			return 
		}
		if  !=  {
			.WriteRune()
			.rewind()
			continue
		}
		.commit()
		return 
	}
}

func ( *parser) () *d2ast.Array {
	 := &d2ast.Array{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.Subtract('[', .utf16Pos),
		},
	}
	defer .Range.End.From(&.readerPos)

	.depth++
	defer dec(&.depth)

	for {
		,  := .readNotSpace()
		if  {
			.errorf(.Range.Start, .readerPos, "arrays must be terminated with ]")
			return 
		}

		switch  {
		case ';':
			continue
		case ']':
			return 
		}

		 := .parseArrayNode()
		if .Unbox() != nil {
			.Nodes = append(.Nodes, )
		}

		if .BlockComment != nil {
			// Anything after a block comment is ok.
			continue
		}

		 := .pos
		for {
			, ,  := .peekNotSpace()
			if  ||  != 0 ||  == ';' ||  == ']' ||  == '#' {
				.rewind()
				break
			}
			.commit()
		}

		if  != .pos {
			if .Unbox() != nil {
				.errorf(, .pos, "unexpected text after %v", .Unbox().Type())
			} else {
				.errorf(, .pos, "invalid text beginning unquoted string")
			}
		}
	}
}

func ( *parser) ( rune) d2ast.ArrayNodeBox {
	var  d2ast.ArrayNodeBox

	switch  {
	case '#':
		.Comment = .parseComment()
		return 
	case '"':
		,  := .peekn(2)
		if  {
			break
		}
		if  != `""` {
			.rewind()
			break
		}
		.commit()
		.BlockComment = .parseBlockComment()
		return 
	case '.':
		,  := .peekn(2)
		if  {
			break
		}
		if  != ".." {
			.rewind()
			break
		}
		,  := .peek()
		if  {
			break
		}
		if  == '$' {
			.commit()
			.Substitution = .parseSubstitution(true)
			return 
		}
		if  == '@' {
			.commit()
			.Import = .parseImport(true)
			return 
		}
		.rewind()
		break
	}

	.replay()
	 := .parseValue()
	if .UnquotedString != nil && .UnquotedString.ScalarString() == "" &&
		!(len(.UnquotedString.Value) > 0 && .UnquotedString.Value[0].Substitution != nil) {
		.errorf(.pos, .pos.Advance(, .utf16Pos), "unquoted strings cannot start on %q", )
	}
	.Null = .Null
	.Boolean = .Boolean
	.Number = .Number
	.UnquotedString = .UnquotedString
	.DoubleQuotedString = .DoubleQuotedString
	.SingleQuotedString = .SingleQuotedString
	.BlockString = .BlockString
	.Array = .Array
	.Map = .Map
	.Import = .Import
	return 
}

func ( *parser) () d2ast.ValueBox {
	var  d2ast.ValueBox

	, ,  := .peekNotSpace()
	if  ||  > 0 {
		.rewind()
		return 
	}
	.commit()

	switch  {
	case '[':
		.Array = .parseArray()
		return 
	case '{':
		.Map = .parseMap(false)
		return 
	case '@':
		.Import = .parseImport(false)
		return 
	}

	.replay()
	 := .parseString(false)
	if .DoubleQuotedString != nil {
		.DoubleQuotedString = .DoubleQuotedString
		return 
	}
	if .SingleQuotedString != nil {
		.SingleQuotedString = .SingleQuotedString
		return 
	}
	if .BlockString != nil {
		.BlockString = .BlockString
		return 
	}

	if .UnquotedString == nil {
		return 
	}

	 := .UnquotedString
	if strings.EqualFold(.ScalarString(), "null") {
		.Null = &d2ast.Null{
			Range: .Range,
		}
		return 
	}

	if strings.EqualFold(.ScalarString(), "true") {
		.Boolean = &d2ast.Boolean{
			Range: .Range,
			Value: true,
		}
		return 
	}

	if strings.EqualFold(.ScalarString(), "false") {
		.Boolean = &d2ast.Boolean{
			Range: .Range,
			Value: false,
		}
		return 
	}

	// TODO: only if matches regex
	,  := big.NewRat(0, 1).SetString(.ScalarString())
	if  {
		.Number = &d2ast.Number{
			Range: .Range,
			Raw:   .ScalarString(),
			Value: ,
		}
		return 
	}

	.UnquotedString = 
	return 
}

func ( *parser) ( bool) *d2ast.Substitution {
	 := &d2ast.Substitution{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.SubtractString("$", .utf16Pos),
		},
		Spread: ,
	}
	defer .Range.End.From(&.pos)

	if .Spread {
		.Range.Start = .Range.Start.SubtractString("...", .utf16Pos)
	}

	, ,  := .peekNotSpace()
	if  {
		return nil
	}
	if  > 0 {
		.rewind()
		return nil
	}
	if  != '{' {
		.rewind()
		.errorf(.Range.Start, .readerPos, "substitutions must begin on {")
		return nil
	} else {
		.commit()
	}

	 := .parseKey()
	if  != nil {
		.Path = .Path
	}

	, ,  = .peekNotSpace()
	if  {
		.errorf(.Range.Start, .readerPos, "substitutions must be terminated by }")
		return 
	}
	if  > 0 ||  != '}' {
		.rewind()
		.errorf(.Range.Start, .pos, "substitutions must be terminated by }")
		return 
	}
	.commit()

	return 
}

func ( *parser) ( bool) *d2ast.Import {
	 := &d2ast.Import{
		Range: d2ast.Range{
			Path:  .path,
			Start: .pos.SubtractString("$", .utf16Pos),
		},
		Spread: ,
	}
	defer .Range.End.From(&.pos)

	if .Spread {
		.Range.Start = .Range.Start.SubtractString("...", .utf16Pos)
	}

	var  strings.Builder
	for {
		,  := .peek()
		if  {
			break
		}
		if  != '.' &&  != '/' {
			.rewind()
			break
		}
		.WriteRune()
		.commit()
	}
	.Pre = .String()

	 := .parseKey()
	if  == nil {
		return 
	}
	if .Path[0].UnquotedString != nil && len(.Path) > 1 && .Path[1].UnquotedString != nil && .Path[1].Unbox().ScalarString() == "d2" {
		.Path = append(.Path[:1], .Path[2:]...)
	}
	.Path = .Path
	return 
}

// func marshalKey(k *d2ast.Key) string {
// 	var sb strings.Builder
// 	for i, s := range k.Path {
// 		// TODO: Need to encode specials and quotes.
// 		sb.WriteString(s.Unbox().ScalarString())
// 		if i < len(k.Path)-1 {
// 			sb.WriteByte('.')
// 		}
// 	}
// 	return sb.String()
// }

func dec( *int) {
	* -= 1
}

func ( *parser) () string {
	return strings.Repeat(" ", .depth*2)
}

func trimIndent(,  string) string {
	 := strings.Split(, "\n")
	for ,  := range  {
		if  == "" {
			continue
		}
		_,  = splitLeadingIndent(, len())
		[] = 
	}
	return strings.Join(, "\n")
}

func trimCommonIndent( string) string {
	 := ""
	for ,  := range strings.Split(, "\n") {
		if  == "" {
			continue
		}
		,  := splitLeadingIndent(, -1)
		if  == "" {
			// No common indent return as is.
			return 
		}
		if  == "" {
			// Whitespace only line.
			continue
		}
		if  == "" || len() < len() {
			 = 
		}
	}
	if  == "" {
		return 
	}
	return trimIndent(, )
}

func splitLeadingIndent( string,  int) (,  string) {
	var  strings.Builder
	 := 0
	for ,  := range  {
		if !unicode.IsSpace() {
			break
		}
		++
		if  != '\t' {
			.WriteRune()
		} else {
			.WriteByte(' ')
			.WriteByte(' ')
		}
		if  > -1 && .Len() ==  {
			break
		}
	}
	return .String(), [:]
}