This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+10
View File
@@ -1,5 +1,7 @@
# HuJSON - "Human JSON" ([JWCC](https://nigeltao.github.io/blog/2021/json-with-commas-comments.html))
[![Go Reference](https://pkg.go.dev/badge/github.com/tailscale/hujson.svg)](https://pkg.go.dev/github.com/tailscale/hujson)
The `github.com/tailscale/hujson` package implements
the [JWCC](https://nigeltao.github.io/blog/2021/json-with-commas-comments.html) extension
of [standard JSON](https://datatracker.ietf.org/doc/html/rfc8259).
@@ -15,6 +17,14 @@ For details, see the JWCC docs at:
https://nigeltao.github.io/blog/2021/json-with-commas-comments.html
## Formatting with hujsonfmt
`hujsonfmt` is a program that formats HuJSON files. Install it by running:
```
go install github.com/tailscale/hujson/cmd/hujsonfmt@latest
```
## Visual Studio Code association
Visual Studio Code supports a similar `jsonc` (JSON with comments) format. To
+15 -25
View File
@@ -88,21 +88,6 @@ func (v *Value) Format() {
v.UpdateOffsets()
}
// Range iterates through a Value in depth-first order and
// calls f for each value (including the root value).
// It stops iteration when f returns false.
func (v *Value) Range(f func(v *Value) bool) bool {
if !f(v) {
return false
}
if comp, ok := v.Value.(composite); ok {
return comp.rangeValues(func(v2 *Value) bool {
return v2.Range(f)
})
}
return true
}
// normalize performs simple normalization changes. In particular, it:
// - normalizes strings,
// - normalizes empty objects and arrays as simply {} or [],
@@ -129,15 +114,16 @@ func (v *Value) normalize() bool {
// If there is only whitespace between the name and colon,
// or between the value and comma, then remove the whitespace.
v2.rangeValues(func(v *Value) bool {
if !v.AfterExtra.hasComment() {
v.AfterExtra = nil
for v3 := range v2.allValues() {
if !v3.AfterExtra.hasComment() {
v3.AfterExtra = nil
}
return true
})
}
// Normalize all sub-values.
v2.rangeValues((*Value).normalize)
for v3 := range v2.allValues() {
v3.normalize()
}
}
return true
}
@@ -583,7 +569,9 @@ func (v *Value) alignObjectValues() bool {
// Recursively align all sub-objects.
if comp, ok := v.Value.(composite); ok {
comp.rangeValues((*Value).alignObjectValues)
for v2 := range comp.allValues() {
v2.alignObjectValues()
}
}
return true
}
@@ -593,9 +581,11 @@ func (v Value) hasNewline(checkTopLevelExtra bool) bool {
return true
}
if comp, ok := v.Value.(composite); ok {
return !comp.rangeValues(func(v *Value) bool {
return !v.hasNewline(true)
})
for v := range comp.allValues() {
if v.hasNewline(true) {
return true
}
}
}
return false
}
+1 -1
View File
@@ -1,4 +1,4 @@
go 1.18
go 1.26
use (
.
+20
View File
@@ -221,13 +221,24 @@ func consumeExtra(n int, b []byte) (int, error) {
n += consumeWhitespace(b[n:])
// Skip past comments.
case '/':
isLineComment := bytes.HasPrefix(b[n:], lineCommentStart)
switch nc := consumeComment(b[n:]); {
case nc == 0:
return n, nil
case nc < 0:
if isLineComment {
if i := indexLineSeparator(b[n:]); i >= 0 {
return n + i, newInvalidCharacterError(b[n+i:], "in line comment")
}
}
return n, fmt.Errorf("parsing comment: %w", io.ErrUnexpectedEOF)
case !utf8.Valid(b[n : n+nc]):
return n, fmt.Errorf("invalid UTF-8 in comment")
case isLineComment:
if i := indexLineSeparator(b[n : n+nc]); i >= 0 {
return n + i, newInvalidCharacterError(b[n+i:], "in line comment")
}
n += nc
default:
n += nc
}
@@ -245,6 +256,15 @@ func consumeWhitespace(b []byte) (n int) {
return n
}
func indexLineSeparator(b []byte) int {
i := bytes.IndexRune(b, '\u2028')
j := bytes.IndexRune(b, '\u2029')
if i < 0 || (j >= 0 && j < i) {
return j
}
return i
}
// consumeComment consumes a line or block comment start in b.
// It returns the length of the comment if valid, otherwise
// it returns 0 if it is not a comment and -1 if it is invalid.
+2 -2
View File
@@ -417,8 +417,8 @@ func (b *Extra) extractTrailingcomments(readonly bool) (trailing Extra) {
// classifyComments classifies comments as belonging to the previous element
// or belonging to the current element such that:
// * b[:prevEnd] belongs to the previous element, and
// * b[currStart:] belongs to the current element.
// - b[:prevEnd] belongs to the previous element, and
// - b[currStart:] belongs to the current element.
//
// Invariant: prevEnd <= currStart
func (b Extra) classifyComments() (prevEnd, currStart int) {
+12 -8
View File
@@ -14,8 +14,10 @@ func (v *Value) isStandard() bool {
return false
}
if comp, ok := v.Value.(composite); ok {
if !comp.rangeValues((*Value).isStandard) {
return false
for v2 := range comp.allValues() {
if !v2.isStandard() {
return false
}
}
if hasTrailingComma(comp) || !comp.afterExtra().IsStandard() {
return false
@@ -41,15 +43,16 @@ func (v *Value) Minimize() {
v.minimize()
v.UpdateOffsets()
}
func (v *Value) minimize() bool {
func (v *Value) minimize() {
v.BeforeExtra = nil
if v2, ok := v.Value.(composite); ok {
v2.rangeValues((*Value).minimize)
for v3 := range v2.allValues() {
v3.minimize()
}
setTrailingComma(v2, false)
*v2.afterExtra() = nil
}
v.AfterExtra = nil
return true
}
// Standardize strips any features specific to HuJSON from v,
@@ -60,10 +63,12 @@ func (v *Value) Standardize() {
v.standardize()
v.UpdateOffsets() // should be noop if offsets are already correct
}
func (v *Value) standardize() bool {
func (v *Value) standardize() {
v.BeforeExtra.standardize()
if comp, ok := v.Value.(composite); ok {
comp.rangeValues((*Value).standardize)
for v2 := range comp.allValues() {
v2.standardize()
}
if last := comp.lastValue(); last != nil && last.AfterExtra != nil {
*comp.afterExtra() = append(append(last.AfterExtra, ' '), *comp.afterExtra()...)
last.AfterExtra = nil
@@ -71,7 +76,6 @@ func (v *Value) standardize() bool {
comp.afterExtra().standardize()
}
v.AfterExtra.standardize()
return true
}
func (b *Extra) standardize() {
for i, c := range *b {
+74 -20
View File
@@ -12,8 +12,7 @@
//
// See https://nigeltao.github.io/blog/2021/json-with-commas-comments.html
//
//
// Functionality
// # Functionality
//
// The Parse function parses HuJSON input as a Value,
// which is a syntax tree exactly representing the input.
@@ -32,8 +31,7 @@
// but instead for the HuJSON and standard JSON format.
// The Patch method applies a JSON Patch (RFC 6902) to the receiving value.
//
//
// Grammar
// # Grammar
//
// The changes to the JSON grammar are:
//
@@ -72,8 +70,7 @@
// '000A' ws
// '000D' ws
//
//
// Use with the Standard Library
// # Use with the Standard Library
//
// This package operates with HuJSON as an AST. In order to parse HuJSON
// into arbitrary Go types, use this package to parse HuJSON input as an AST,
@@ -95,6 +92,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"iter"
"math"
"strconv"
"unicode/utf8"
@@ -147,6 +145,45 @@ func (v Value) Clone() Value {
return v
}
// Range iterates through a Value in depth-first order and
// calls f for each value (including the root value).
// It stops iteration when f returns false.
//
// Deprecated: Use [All] instead.
func (v *Value) Range(f func(v *Value) bool) bool {
for v2 := range v.All() {
if !f(v2) {
return false
}
}
return true
}
// All returns an iterator over all values in depth-first order,
// starting with v itself.
func (v *Value) All() iter.Seq[*Value] {
return func(yield func(*Value) bool) {
v.walkAll(yield)
}
}
// walkAll recursively yields v and its descendants in depth-first order.
func (v *Value) walkAll(yield func(*Value) bool) bool {
if !yield(v) {
return false
}
if comp, ok := v.Value.(composite); ok {
for v2 := range comp.allValues() {
if !v2.walkAll(yield) {
return false
}
}
}
return true
}
// ValueTrimmed is a JSON value without surrounding whitespace or comments.
// This is a sum type consisting of Literal, *Object, or *Array.
type ValueTrimmed interface {
@@ -335,6 +372,7 @@ type Object struct {
// after the preceding open brace or comma and before the closing brace.
AfterExtra Extra
}
type ObjectMember struct {
Name, Value Value
}
@@ -342,35 +380,43 @@ type ObjectMember struct {
func (obj Object) length() int {
return len(obj.Members)
}
func (obj Object) firstValue() *Value {
if len(obj.Members) > 0 {
return &obj.Members[0].Name
}
return nil
}
func (obj Object) rangeValues(f func(*Value) bool) bool {
for i := range obj.Members {
if !f(&obj.Members[i].Name) {
return false
}
if !f(&obj.Members[i].Value) {
return false
// allValues iterates all members of the object,
// interleaved between the member name and the member value.
func (obj Object) allValues() iter.Seq[*Value] {
return func(yield func(*Value) bool) {
for i := range obj.Members {
if !yield(&obj.Members[i].Name) {
return
}
if !yield(&obj.Members[i].Value) {
return
}
}
}
return true
}
func (obj Object) lastValue() *Value {
if len(obj.Members) > 0 {
return &obj.Members[len(obj.Members)-1].Value
}
return nil
}
func (obj *Object) beforeExtraAt(i int) *Extra {
if i < len(obj.Members) {
return &obj.Members[i].Name.BeforeExtra
}
return &obj.AfterExtra
}
func (obj *Object) afterExtra() *Extra {
return &obj.AfterExtra
}
@@ -401,37 +447,45 @@ type Array struct {
// after the preceding open bracket or comma and before the closing bracket.
AfterExtra Extra
}
type ArrayElement = Value
func (arr Array) length() int {
return len(arr.Elements)
}
func (arr Array) firstValue() *Value {
if len(arr.Elements) > 0 {
return &arr.Elements[0]
}
return nil
}
func (arr Array) rangeValues(f func(*Value) bool) bool {
for i := range arr.Elements {
if !f(&arr.Elements[i]) {
return false
// allValues iterates all elements of the array.
func (arr Array) allValues() iter.Seq[*Value] {
return func(yield func(*Value) bool) {
for i := range arr.Elements {
if !yield(&arr.Elements[i]) {
return
}
}
}
return true
}
func (arr Array) lastValue() *Value {
if len(arr.Elements) > 0 {
return &arr.Elements[len(arr.Elements)-1]
}
return nil
}
func (arr *Array) beforeExtraAt(i int) *Extra {
if i < len(arr.Elements) {
return &arr.Elements[i].BeforeExtra
}
return &arr.AfterExtra
}
func (arr *Array) afterExtra() *Extra {
return &arr.AfterExtra
}
@@ -457,7 +511,7 @@ type composite interface {
length() int
firstValue() *Value
rangeValues(func(*Value) bool) bool
allValues() iter.Seq[*Value]
lastValue() *Value
getAt(int) ValueTrimmed