🏡 index : quine-global/quintodrome.git

// Package jsoncommentstrip provides an io.Reader that strips JavaScript-style
// comments (// line and /* block */) from JSON input while preserving
// comment-like sequences inside JSON string values.
package jsoncommentstrip

import (
	"bufio"
	"io"
)

type state int

const (
	stateNormal state = iota
	stateInString
	stateInStringEscape
	stateMaybeComment  // saw '/'
	stateLineComment   // inside // ...
	stateBlockComment  // inside /* ... */
	stateMaybeBlockEnd // saw '*' inside block comment
)

type reader struct {
	r     *bufio.Reader
	state state
}

// NewReader returns an io.Reader that strips JSON comments from the
// underlying reader. It removes single-line comments (// to end of line)
// and block comments (/* ... */), while preserving comment-like sequences
// that appear inside JSON string values.
func NewReader(r io.Reader) io.Reader {
	return &reader{
		r:     bufio.NewReader(r),
		state: stateNormal,
	}
}

func (cr *reader) Read(p []byte) (int, error) {
	n := 0
	for n < len(p) {
		b, err := cr.r.ReadByte()
		if err != nil {
			if cr.state == stateMaybeComment {
				// Emit the pending '/' before returning EOF
				p[n] = '/'
				n++
				cr.state = stateNormal
			}
			return n, err
		}

		switch cr.state {
		case stateNormal:
			switch b {
			case '"':
				p[n] = b
				n++
				cr.state = stateInString
			case '/':
				cr.state = stateMaybeComment
			default:
				p[n] = b
				n++
			}

		case stateInString:
			p[n] = b
			n++
			switch b {
			case '\\':
				cr.state = stateInStringEscape
			case '"':
				cr.state = stateNormal
			}

		case stateInStringEscape:
			p[n] = b
			n++
			cr.state = stateInString

		case stateMaybeComment:
			switch b {
			case '/':
				cr.state = stateLineComment
			case '*':
				cr.state = stateBlockComment
			default:
				// The '/' was not a comment start; emit it and the current byte
				p[n] = '/'
				n++
				if n < len(p) {
					p[n] = b
					n++
				} else {
					// We need to "unread" the current byte since buffer is full
					_ = cr.r.UnreadByte()
				}
				cr.state = stateNormal
			}

		case stateLineComment:
			if b == '\n' || b == '\r' {
				p[n] = b
				n++
				cr.state = stateNormal
			}
			// Otherwise, consume and discard

		case stateBlockComment:
			if b == '*' {
				cr.state = stateMaybeBlockEnd
			}
			// Otherwise, consume and discard

		case stateMaybeBlockEnd:
			if b == '/' {
				cr.state = stateNormal
			} else if b == '*' {
				// Stay in stateMaybeBlockEnd (consecutive *'s)
				cr.state = stateMaybeBlockEnd
			} else {
				cr.state = stateBlockComment
			}
		}
	}
	return n, nil
}