Ctrl K
ComunidadGuías…

vlib/json2/json5_scanner.v

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Copyright (c) 2026 The V Language. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module json2

import encoding.utf8

// rune literals for the ASCII characters the scanner tests against. Runes are
// compared as `int` because V promotes a `rune` literal comparison to a string
// comparison, which is both slower and wrong for code points above 0x7F.
// ASCII code points the scanner compares against. They are compared as `int`
// because V promotes a `rune` comparison to a string comparison, which is both
// slower and wrong for code points above 0x7F.
const r_star = 0x2A
const r_plus = 0x2B
const r_upper_a = 0x41
const r_comma = 0x2C
const r_minus = 0x2D
const r_dot = 0x2E
const r_slash = 0x2F
const r_zero = 0x30
const r_upper_e = 0x45
const r_upper_f = 0x46
const r_upper_i = 0x49
const r_upper_n = 0x4E
const r_colon = 0x3A
const r_semicolon = 0x3B
const r_nine = 0x39
const r_lt = 0x3C
const r_upper_x = 0x58
const r_lbkt = 0x5B
const r_backslash = 0x5C
const r_rbkt = 0x5D
const r_underscr = 0x5F
const r_lower_a = 0x61
const r_lower_b = 0x62
const r_lower_e = 0x65
const r_lower_f = 0x66
const r_lower_n = 0x6E
const r_lower_r = 0x72
const r_lower_t = 0x74
const r_lower_u = 0x75
const r_lower_v = 0x76
const r_lower_x = 0x78
const r_lcbr = 0x7B
const r_rcbr = 0x7D
const r_quote_dbl = 0x22
const r_dollar = 0x24
const r_apostroph = 0x27
const r_space = 0x20
const r_tab = 0x09
const r_lf = 0x0A
const r_vt = 0x0B
const r_ff = 0x0C
const r_cr = 0x0D
const r_nbsp = 0xA0
const r_zwnj = 0x200C
const r_zwj = 0x200D
const r_line_sep = 0x2028
const r_para_sep = 0x2029
const r_bom = 0xFEFF

// Json5TokenKind identifies the kind of a JSON5 token.
pub enum Json5TokenKind {
	none
	error
	comment
	str
	ident
	number
	bool
	null
	infinity
	nan
	comma = 0x2C // ,
	colon = 0x3A // :
	lsbr  = 0x5B // [
	rsbr  = 0x5D // ]
	lcbr  = 0x7B // {
	rcbr  = 0x7D // }
	eof
}

// Json5Pos is a location inside the scanned text.
pub struct Json5Pos {
	offset int // Byte offset in the original input, including a leading BOM.
pub:
	line int
	col  int
}

// Json5Token is a single lexical unit produced by the Json5Scanner.
pub struct Json5Token {
pub:
	kind   Json5TokenKind
	lit    string // Decoded value for strings and identifiers, raw source text otherwise.
	pos    Json5Pos
	length int // Token length in source bytes, before decoding escapes.
}

// Json5Scanner tokenizes JSON5 text. Whitespace and comments are treated as trivia
// and skipped; every other token is reported to the caller.
pub struct Json5Scanner {
	source string
pub:
	text []rune
mut:
	pos    int
	offset int
	line   int = 1
	col    int = 1
}

// new_json5_scanner creates a Json5Scanner over `text`, dropping a leading byte order mark.
pub fn new_json5_scanner(text string) &Json5Scanner {
	mut runes := text.runes()
	if runes.len > 0 && int(runes[0]) == r_bom {
		runes.delete(0)
	}
	return &Json5Scanner{
		source: text
		text:   runes
		offset: if text.starts_with('\uFEFF') { 3 } else { 0 }
	}
}

// peek returns the rune `offset` positions ahead, or -1 at the end of the input.
fn (s &Json5Scanner) peek(offset int) rune {
	i := s.pos + offset
	if i < 0 || i >= s.text.len {
		return -1
	}
	return s.text[i]
}

// peek_code returns the code point `offset` positions ahead, or -1 at the end of
// the input.
fn (s &Json5Scanner) peek_code(offset int) int {
	r := s.peek(offset)
	return if r == -1 { -1 } else { int(r) }
}

// advance consumes the current rune and keeps the line/column counters in sync.
fn (mut s Json5Scanner) advance() rune {
	if s.pos >= s.text.len {
		return -1
	}
	ch := s.text[s.pos]
	if int(ch) == 0xfffd && !s.source[s.offset..].starts_with('\uFFFD') {
		// string.runes replaces each invalid source byte with a replacement rune.
		s.offset++
	} else {
		s.offset += ch.length_in_bytes()
	}
	s.pos++
	if json5_is_line_terminator(int(ch)) {
		if int(ch) != r_lf || s.pos < 2 || int(s.text[s.pos - 2]) != r_cr {
			s.line++
		}
		s.col = 1
	} else {
		s.col++
	}
	return ch
}

// position returns the current position.
fn (s &Json5Scanner) position() Json5Pos {
	return Json5Pos{
		offset: s.offset
		line:   s.line
		col:    s.col
	}
}

// json5_is_digit reports whether `code` is an ASCII decimal digit.
fn json5_is_digit(code int) bool {
	return code >= r_zero && code <= r_nine
}

// json5_is_hex_digit reports whether `code` is an ASCII hexadecimal digit.
fn json5_is_hex_digit(code int) bool {
	return (code >= r_zero && code <= r_nine) || (code >= r_lower_a && code <= r_lower_f)
		|| (code >= r_upper_a && code <= r_upper_f)
}

// json5_hex_digit_value returns the numeric value of an ASCII hexadecimal digit.
fn json5_hex_digit_value(code int) int {
	if code <= r_nine {
		return code - r_zero
	}
	if code <= r_upper_f {
		return code - r_upper_a + 10
	}
	return code - r_lower_a + 10
}

// json5_is_whitespace reports whether `code` is JSON5 whitespace: the ASCII space
// characters, the no-break space, and any unicode space separator.
fn json5_is_whitespace(code int) bool {
	return code == r_space || code == r_tab || code == r_lf || code == r_cr
		|| code == r_vt || code == r_ff || code == r_nbsp || code == r_bom
		|| (code > 0xff && utf8.is_space(rune(code)))
}

// json5_is_line_terminator reports whether `code` ends a line for the purposes of `//`
// comments and escaped newlines in strings.
fn json5_is_line_terminator(code int) bool {
	return code == r_lf || code == r_cr || code == r_line_sep || code == r_para_sep
}

// json5_is_ident_start reports whether `code` may start an unquoted key.
fn json5_is_ident_start(code int) bool {
	if code < 0 || code > 0x10ffff {
		return false
	}
	return code == r_dollar || code == r_underscr || utf8.is_letter(rune(code))
		|| json5_in_unicode_ranges(code, json5_letter_number_ranges)
}

// json5_is_ident_part reports whether `code` may continue an unquoted key. On top of
// `json5_is_ident_start` it allows Mn/Mc marks, Nd digits, Pc connectors and joiners.
fn json5_is_ident_part(code int) bool {
	if json5_is_ident_start(code) || json5_is_digit(code) {
		return true
	}
	return code == r_zwnj || code == r_zwj
		|| json5_in_unicode_ranges(code, json5_identifier_part_ranges)
}

// json5_in_unicode_ranges searches sorted inclusive start/end pairs.
fn json5_in_unicode_ranges(code int, ranges [][2]int) bool {
	mut low := 0
	mut high := ranges.len
	for low < high {
		middle := low + (high - low) / 2
		if code < ranges[middle][0] {
			high = middle
		} else if code > ranges[middle][1] {
			low = middle + 1
		} else {
			return true
		}
	}
	return false
}

// skip_trivia consumes whitespace and comments.
fn (mut s Json5Scanner) skip_trivia() ! {
	for s.pos < s.text.len {
		code := int(s.text[s.pos])
		if json5_is_whitespace(code) {
			s.advance()
			continue
		}
		if code == r_slash && s.peek_code(1) == r_slash {
			s.advance()
			s.advance()
			for s.pos < s.text.len && !json5_is_line_terminator(int(s.text[s.pos])) {
				s.advance()
			}
			continue
		}
		if code == r_slash && s.peek_code(1) == r_star {
			start := s.position()
			s.advance()
			s.advance()
			mut closed := false
			for s.pos < s.text.len {
				if int(s.text[s.pos]) == r_star && s.peek_code(1) == r_slash {
					s.advance()
					s.advance()
					closed = true
					break
				}
				s.advance()
			}
			if !closed {
				return json5_syntax_error('unterminated block comment', start.line, start.col)
			}
			continue
		}
		return
	}
}

// next returns the next token, decoding string and identifier escapes and skipping trivia.
@[manualfree]
pub fn (mut s Json5Scanner) next() !Json5Token {
	s.skip_trivia()!
	start := s.position()
	if s.pos >= s.text.len {
		return Json5Token{
			kind: .eof
			pos:  start
		}
	}
	code := int(s.text[s.pos])
	match code {
		r_lcbr, r_rcbr, r_lbkt, r_rbkt, r_comma, r_colon {
			ch := s.advance()
			return Json5Token{
				kind:   unsafe { Json5TokenKind(code) }
				lit:    ch.str()
				pos:    start
				length: s.offset - start.offset
			}
		}
		r_quote_dbl {
			return s.scan_string(start, r_quote_dbl)
		}
		r_apostroph {
			return s.scan_string(start, r_apostroph)
		}
		else {}
	}
	if json5_is_digit(code) || code == r_minus || code == r_plus || code == r_dot {
		return s.scan_number(start)
	}
	if json5_is_ident_start(code) || code == r_backslash {
		return s.scan_ident(start)
	}
	return json5_syntax_error('unexpected token `${s.text[s.pos].str()}`', start.line, start.col)
}

// scan_string scans a single- or double-quoted string and returns its decoded
// value.
@[manualfree]
fn (mut s Json5Scanner) scan_string(start Json5Pos, quote_code int) !Json5Token {
	s.advance() // opening quote
	mut out := []u8{cap: 32}
	unsafe { out.flags |= .noslices }
	// The returned UTF-8 view borrows this explicitly owned scratch buffer.
	mut utf8_buffer := unsafe { malloc_noscan(5) }
	defer {
		unsafe {
			out.free()
			free(utf8_buffer)
		}
	}
	for {
		if s.pos >= s.text.len {
			return json5_syntax_error('unterminated string', start.line, start.col)
		}
		code := int(s.text[s.pos])
		mut character := rune(code)
		if code == quote_code {
			s.advance()
			break
		}
		if code == r_backslash {
			s.advance()
			if s.pos >= s.text.len {
				return json5_syntax_error('unterminated escape sequence', start.line, start.col)
			}
			esc := int(s.text[s.pos])
			if json5_is_digit(esc) && (esc != r_zero || json5_is_digit(s.peek_code(1))) {
				return json5_syntax_error('invalid decimal digit in string escape', s.line, s.col)
			}
			if json5_is_line_terminator(esc) {
				// A backslash before a line terminator is a line continuation: it
				// and the terminator contribute nothing to the value.
				if esc == r_cr && s.peek_code(1) == r_lf {
					s.advance()
				}
				s.advance()
				continue
			}
			s.advance()
			match esc {
				r_lower_n { character = rune(0x0A) }
				r_lower_t { character = rune(0x09) }
				r_lower_r { character = rune(0x0D) }
				r_lower_b { character = rune(0x08) }
				r_lower_f { character = rune(0x0C) }
				r_lower_v { character = rune(0x0B) }
				r_zero { character = rune(0x00) }
				r_lower_x { character = s.scan_hex_digits(2, start)! }
				r_lower_u { character = s.scan_unicode_escape(start)! }
				else {
					// Other non-digit characters yield the escaped character itself.
					character = rune(esc)
				}
			}
		} else {
			// JSON5 permits literal U+2028/U+2029 in strings (specification section 5.2).
			if code == r_lf || code == r_cr {
				return json5_syntax_error('unescaped line terminator in string', s.line, s.col)
			}
			character = s.advance()
		}
		// This view borrows only the scratch UTF-8 buffer and is copied immediately.
		encoded := unsafe { utf32_to_str_no_malloc(u32(character), mut utf8_buffer) }
		for byte in encoded {
			out << byte
		}
	}
	return Json5Token{
		kind:   .str
		lit:    out.bytestr()
		pos:    start
		length: s.offset - start.offset
	}
}

// scan_hex_digits reads `count` hexadecimal digits and returns them as a rune.
fn (mut s Json5Scanner) scan_hex_digits(count int, start Json5Pos) !rune {
	mut value := u32(0)
	for _ in 0 .. count {
		if s.pos >= s.text.len {
			return json5_syntax_error('incomplete hexadecimal escape', start.line, start.col)
		}
		code := int(s.text[s.pos])
		if !json5_is_hex_digit(code) {
			return json5_syntax_error('`${s.text[s.pos].str()}` is not a hexadecimal digit',
				s.line, s.col)
		}
		value = value << 4 | u32(json5_hex_digit_value(code))
		s.advance()
	}
	return rune(value)
}

// scan_unicode_escape reads either `\uXXXX` or the ES6 `\u{...}` form.
fn (mut s Json5Scanner) scan_unicode_escape(start Json5Pos) !rune {
	if s.peek_code(0) == r_lcbr {
		s.advance()
		mut value := u32(0)
		mut digits := 0
		for s.pos < s.text.len && int(s.text[s.pos]) != r_rcbr {
			code := int(s.text[s.pos])
			if !json5_is_hex_digit(code) {
				return json5_syntax_error('`${s.text[s.pos].str()}` is not a hexadecimal digit',
					s.line, s.col)
			}
			value = value << 4 | u32(json5_hex_digit_value(code))
			digits++
			s.advance()
		}
		if s.pos >= s.text.len {
			return json5_syntax_error('unterminated `\\u{...}` escape', start.line, start.col)
		}
		s.advance() // `}`
		if digits == 0 {
			return json5_syntax_error('empty `\\u{...}` escape', start.line, start.col)
		}
		if value > 0x10FFFF || value >= 0xD800 && value <= 0xDFFF {
			return json5_syntax_error('`\\u{...}` escape is out of the unicode range', start.line,
				start.col)
		}
		return rune(value)
	}
	first := s.scan_hex_digits(4, start)!
	if first >= 0xD800 && first <= 0xDBFF {
		if s.peek_code(0) != r_backslash || s.peek_code(1) != r_lower_u {
			return json5_syntax_error('high surrogate requires a low surrogate escape', start.line,
				start.col)
		}
		s.advance()
		s.advance()
		second := s.scan_hex_digits(4, start)!
		if second < 0xDC00 || second > 0xDFFF {
			return json5_syntax_error('high surrogate requires a low surrogate escape', start.line,
				start.col)
		}
		return rune(0x10000 + (u32(first) - 0xD800) * 0x400 + u32(second) - 0xDC00)
	}
	if first >= 0xDC00 && first <= 0xDFFF {
		return json5_syntax_error('low surrogate requires a preceding high surrogate', start.line,
			start.col)
	}
	return first
}

// scan_ident scans an unquoted key, or one of the keywords `true`, `false`,
// `null`, `Infinity` and `NaN`.
@[manualfree]
fn (mut s Json5Scanner) scan_ident(start Json5Pos) !Json5Token {
	mut out := []u8{}
	unsafe { out.flags |= .noslices }
	// The returned UTF-8 view borrows this explicitly owned scratch buffer.
	mut utf8_buffer := unsafe { malloc_noscan(5) }
	mut has_escape := false
	defer {
		unsafe {
			out.free()
			free(utf8_buffer)
		}
	}
	for s.pos < s.text.len {
		code := int(s.text[s.pos])
		if code == r_backslash {
			escape := s.position()
			is_start := s.offset == start.offset
			if !has_escape {
				for i in start.offset .. s.offset {
					out << s.source[i]
				}
				has_escape = true
			}
			s.advance()
			if s.peek_code(0) != r_lower_u {
				return json5_syntax_error('identifier escape must use `\\uXXXX`', escape.line,
					escape.col)
			}
			s.advance()
			character := s.scan_hex_digits(4, escape)!
			valid := if is_start {
				json5_is_ident_start(int(character))
			} else {
				json5_is_ident_part(int(character))
			}
			if !valid {
				return json5_syntax_error('invalid character in identifier escape', escape.line,
					escape.col)
			}
			// This view borrows only the scratch UTF-8 buffer and is copied immediately.
			encoded := unsafe { utf32_to_str_no_malloc(u32(character), mut utf8_buffer) }
			for byte in encoded {
				out << byte
			}
		} else if json5_is_ident_part(code) {
			offset := s.offset
			s.advance()
			if has_escape {
				for i in offset .. s.offset {
					out << s.source[i]
				}
			}
		} else {
			break
		}
	}
	lit := if has_escape { out.bytestr() } else { s.source.substr(start.offset, s.offset) }
	// Escaped keywords are IdentifierNames for keys, rather than value literals.
	kind := if has_escape {
		Json5TokenKind.ident
	} else {
		match lit {
			'true', 'false' { Json5TokenKind.bool }
			'null' { Json5TokenKind.null }
			'Infinity' { Json5TokenKind.infinity }
			'NaN' { Json5TokenKind.nan }
			else { Json5TokenKind.ident }
		}
	}
	return Json5Token{
		kind:   kind
		lit:    lit
		pos:    start
		length: s.offset - start.offset
	}
}

// scan_number scans a JSON5 number: an optional sign, then either `Infinity`,
// `NaN`, a hexadecimal integer, or a decimal value with an optional leading or
// trailing dot and an optional exponent.
@[manualfree]
fn (mut s Json5Scanner) scan_number(start Json5Pos) !Json5Token {
	code := int(s.text[s.pos])
	if code == r_plus || code == r_minus {
		s.advance()
	}
	// `Infinity` and `NaN` may carry a sign; they are scanned as identifiers so
	// that a bare occurrence in key position still lexes as a key.
	if s.pos >= s.text.len {
		return json5_syntax_error('number has no digits', start.line, start.col)
	}
	first := int(s.text[s.pos])
	if first == r_upper_i || first == r_upper_n {
		tok := s.scan_ident(s.position())!
		defer {
			unsafe { tok.lit.free() }
		}
		return Json5Token{
			kind:   tok.kind
			lit:    s.source.substr(start.offset, s.offset)
			pos:    start
			length: s.offset - start.offset
		}
	}
	if int(s.text[s.pos]) == r_zero && (s.peek_code(1) == r_lower_x || s.peek_code(1) == r_upper_x) {
		s.advance()
		s.advance()
		mut digits := 0
		for s.pos < s.text.len && json5_is_hex_digit(int(s.text[s.pos])) {
			s.advance()
			digits++
		}
		if digits == 0 {
			return json5_syntax_error('hexadecimal literal has no digits', start.line, start.col)
		}
		return Json5Token{
			kind:   .number
			lit:    s.source.substr(start.offset, s.offset)
			pos:    start
			length: s.offset - start.offset
		}
	}
	if first == r_zero && json5_is_digit(s.peek_code(1)) {
		return json5_syntax_error('decimal literal has a leading zero', start.line, start.col)
	}
	mut int_digits := 0
	for s.pos < s.text.len && json5_is_digit(int(s.text[s.pos])) {
		s.advance()
		int_digits++
	}
	// A dot is part of the number both as a decimal separator and as a leading
	// dot (`.5`), and it may also be trailing (`5.`).
	if s.pos < s.text.len && int(s.text[s.pos]) == r_dot {
		s.advance()
		for s.pos < s.text.len && json5_is_digit(int(s.text[s.pos])) {
			s.advance()
			int_digits++
		}
	}
	if int_digits == 0 {
		return json5_syntax_error('number has no digits', start.line, start.col)
	}
	if s.pos < s.text.len && (int(s.text[s.pos]) == r_lower_e || int(s.text[s.pos]) == r_upper_e) {
		s.advance()
		if s.pos < s.text.len && (int(s.text[s.pos]) == r_minus || int(s.text[s.pos]) == r_plus) {
			s.advance()
		}
		mut exp_digits := 0
		for s.pos < s.text.len && json5_is_digit(int(s.text[s.pos])) {
			s.advance()
			exp_digits++
		}
		if exp_digits == 0 {
			return json5_syntax_error('exponent has no digits', start.line, start.col)
		}
	}
	return Json5Token{
		kind:   .number
		lit:    s.source.substr(start.offset, s.offset)
		pos:    start
		length: s.offset - start.offset
	}
}