clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / pdfexport / jsonl.go
jsonl.go 4.9K
1package pdfexport23import (4 "bufio"5 "bytes"6 "encoding/json"7 "fmt"8 "os"9 "path/filepath"10 "slices"11 "strings"12)1314const ExportSchemaV1 = "puzzletea.export.v1"1516type JSONLRecord struct {17 Schema string `json:"schema"`18 Pack JSONLPackMeta `json:"pack"`19 Puzzle JSONLPuzzle `json:"puzzle"`20}2122type JSONLPackMeta struct {23 Generated string `json:"generated"`24 Version string `json:"version"`25 Category string `json:"category"`26 ModeSelection string `json:"mode_selection"`27 Count int `json:"count"`28 Seed string `json:"seed"`29}3031type JSONLPuzzle struct {32 Index int `json:"index"`33 Name string `json:"name"`34 Game string `json:"game"`35 Mode string `json:"mode"`36 Save json.RawMessage `json:"save"`37}3839func ParseJSONLFiles(paths []string) ([]PackDocument, error) {40 docs := make([]PackDocument, 0, len(paths))41 for _, path := range paths {42 doc, err := ParseJSONLFile(path)43 if err != nil {44 return nil, err45 }46 docs = append(docs, doc)47 }48 return docs, nil49}5051func ParseJSONLFile(path string) (PackDocument, error) {52 if !strings.EqualFold(filepath.Ext(path), ".jsonl") {53 return PackDocument{}, fmt.Errorf("%s: expected .jsonl input", path)54 }5556 f, err := os.Open(path)57 if err != nil {58 return PackDocument{}, fmt.Errorf("open input jsonl: %w", err)59 }60 defer f.Close()6162 scanner := bufio.NewScanner(f)63 scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)64 return parseJSONLScanner(path, scanner)65}6667func ParseJSONLData(sourceName string, data []byte) (PackDocument, error) {68 scanner := bufio.NewScanner(bytes.NewReader(data))69 scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)70 return parseJSONLScanner(sourceName, scanner)71}7273func ParseJSONLRecords(sourceName string, records []JSONLRecord) (PackDocument, error) {74 var b strings.Builder75 for _, record := range records {76 line, err := json.Marshal(record)77 if err != nil {78 return PackDocument{}, fmt.Errorf("encode jsonl record: %w", err)79 }80 b.Write(line)81 b.WriteByte('\n')82 }83 return ParseJSONLData(sourceName, []byte(b.String()))84}8586func parseJSONLScanner(path string, scanner *bufio.Scanner) (PackDocument, error) {87 doc := PackDocument{SourcePath: path}88 doc.Metadata.SourceFileName = filepath.Base(path)89 puzzles := []Puzzle{}90 lineNo := 091 seenAny := false92 unsupportedCategories := []string{}9394 for scanner.Scan() {95 lineNo++96 line := strings.TrimSpace(scanner.Text())97 if line == "" {98 continue99 }100 seenAny = true101102 var record JSONLRecord103 if err := json.Unmarshal([]byte(line), &record); err != nil {104 return PackDocument{}, fmt.Errorf("%s:%d: decode jsonl record: %w", path, lineNo, err)105 }106 if record.Schema != ExportSchemaV1 {107 return PackDocument{}, fmt.Errorf("%s:%d: unsupported schema %q", path, lineNo, record.Schema)108 }109110 category := strings.TrimSpace(record.Puzzle.Game)111 if category == "" {112 category = strings.TrimSpace(record.Pack.Category)113 }114 mode := strings.TrimSpace(record.Puzzle.Mode)115 if mode == "" {116 mode = strings.TrimSpace(record.Pack.ModeSelection)117 }118119 p := Puzzle{120 SourcePath: path,121 SourceFileName: filepath.Base(path),122 Category: category,123 ModeSelection: mode,124 Name: record.Puzzle.Name,125 Index: record.Puzzle.Index,126 SaveData: append([]byte(nil), record.Puzzle.Save...),127 }128129 adapter, ok := LookupPrintAdapter(category)130 if !ok {131 if category != "" && !slices.Contains(unsupportedCategories, category) {132 unsupportedCategories = append(unsupportedCategories, category)133 }134 continue135 }136 payload, err := adapter.BuildPDFPayload(p.SaveData)137 if err != nil {138 return PackDocument{}, fmt.Errorf("%s:%d: build print payload: %w", path, lineNo, err)139 }140 if IsNilPrintPayload(payload) {141 continue142 }143 p.PrintPayload = payload144145 if doc.Metadata.GeneratedRaw == "" {146 doc.Metadata.GeneratedRaw = record.Pack.Generated147 doc.Metadata.Version = record.Pack.Version148 doc.Metadata.Count = record.Pack.Count149 doc.Metadata.Seed = record.Pack.Seed150 doc.Metadata.Category = strings.TrimSpace(p.Category)151 if doc.Metadata.Category == "" {152 doc.Metadata.Category = record.Pack.Category153 }154 doc.Metadata.ModeSelection = strings.TrimSpace(p.ModeSelection)155 if doc.Metadata.ModeSelection == "" {156 doc.Metadata.ModeSelection = record.Pack.ModeSelection157 }158 }159160 puzzles = append(puzzles, p)161 }162163 if err := scanner.Err(); err != nil {164 return PackDocument{}, fmt.Errorf("read input jsonl: %w", err)165 }166 if !seenAny {167 return PackDocument{}, fmt.Errorf("%s: input jsonl is empty", path)168 }169 if len(puzzles) == 0 {170 if len(unsupportedCategories) > 0 {171 return PackDocument{}, fmt.Errorf("%s: no printable puzzles found; unsupported categories: %s", path, strings.Join(unsupportedCategories, ", "))172 }173 return PackDocument{}, fmt.Errorf("%s: no printable puzzles found", path)174 }175 if doc.Metadata.Count == 0 {176 doc.Metadata.Count = len(puzzles)177 }178 doc.Puzzles = puzzles179 return doc, nil180}