diff -ruN oxygen_repo/OxygenBasic_reviewed/MASTER_JSON_PARSER_UPDATE.md oxygen_work/MASTER_JSON_PARSER_UPDATE.md
--- oxygen_repo/OxygenBasic_reviewed/MASTER_JSON_PARSER_UPDATE.md	1970-01-01 00:00:00.000000000 +0000
+++ oxygen_work/MASTER_JSON_PARSER_UPDATE.md	2026-04-15 11:29:18.204937428 +0000
@@ -0,0 +1,128 @@
+# OxygenBasic JSON parser update
+
+Date: 2026-04-15
+
+## Scope
+
+This update turns the existing demo-only JSON splitter into a reusable system module.
+
+The parser is now shipped as `inc/JsonUtil.inc` and is also pulled into `inc/ParseUtil.inc`. Because `SysUtil.inc` already depends on `ParseUtil.inc`, the JSON API is also available transitively to existing `SysUtil` consumers.
+
+## Changed files
+
+- `inc/JsonUtil.inc` *(new)*
+- `inc/ParseUtil.inc` *(updated: imports `JsonUtil`)*
+- `demos/DataProcessing/JSONparsing.o2bas` *(updated: rewritten as a system-module demo)*
+- `MASTER_JSON_PARSER_UPDATE.md` *(new)*
+- `MASTER_JSON_PARSER_PATCH.diff` *(new)*
+
+## Public API
+
+### Global helpers
+
+- `JsonEscape(string s) as string`
+  - Escapes a plain string for JSON output.
+
+- `JsonQuote(string s) as string`
+  - Returns a JSON string literal including quotes.
+
+- `JsonKindName(int k) as string`
+  - Converts the internal node kind constant to text.
+
+- `JsonFormat(string s, int pretty=1) as string`
+  - Parse + re-emit JSON in pretty or compact form.
+
+- `JsonMinify(string s) as string`
+  - Compact formatting shortcut.
+
+- `JsonValid(string s) as int`
+  - Returns `1` for valid JSON, otherwise `0`.
+
+### Node kind constants
+
+- `JSON_OBJECT`
+- `JSON_ARRAY`
+- `JSON_STRING`
+- `JSON_NUMBER`
+- `JSON_TRUE`
+- `JSON_FALSE`
+- `JSON_NULL`
+- `JSON_INVALID`
+
+### `JsonDoc` class
+
+#### Parse and status
+
+- `Parse(string s) as int`
+  - Returns the root node id on success, `0` on failure.
+
+- `Valid() as int`
+- `ErrorText() as string`
+- `ErrorPos() as int`
+
+#### Document metadata
+
+- `RootId() as int`
+- `NodeCount() as int`
+
+#### Node inspection
+
+- `NodeKind(int n) as int`
+- `NodeTypeName(int n) as string`
+- `NodeKey(int n) as string`
+- `NodeValue(int n) as string`
+- `ParentId(int n) as int`
+- `NodeLevel(int n) as int`
+- `NodePos(int n) as int`
+- `NodeStop(int n) as int`
+- `FirstChild(int n) as int`
+- `NextSibling(int n) as int`
+- `ItemCount(int n) as int`
+
+#### Navigation
+
+- `FindChild(int parent, string key) as int`
+  - Linear search across direct children.
+
+- `Item(int parent, index) as int`
+  - Zero-based child lookup. Intended for arrays, but works on any child list.
+
+- `Query(string path, int start=0) as int`
+  - Lightweight path helper.
+  - Supported forms:
+    - `$.employees[0].firstName`
+    - `employees[1]`
+    - `[0]` for a root array
+
+#### Output
+
+- `Stringify(int pretty=0) as string`
+  - Rebuilds the current DOM as JSON.
+
+## Behavior notes
+
+- Objects and arrays preserve input order.
+- Strings are decoded for the standard short escapes: `\\`, `\"`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`.
+- `\uXXXX` escapes are decoded directly when the code point fits into a byte value (`<=255`).
+- `\uXXXX` values above `255` are preserved as literal escape text inside the stored string value. This keeps round-tripping stable without forcing a wide-string dependency into the base module.
+- Array indexing in `Item()` and `Query(...[n]...)` is zero-based.
+
+## Minimal usage
+
+```basic
+uses JsonUtil
+
+string s = `{ "items": [ { "id": 10 }, { "id": 20 } ] }`
+new JsonDoc jd
+
+if jd.Parse(s)
+  int n = jd.Query("$.items[1].id")
+  print jd.NodeValue(n)
+endif
+
+del jd
+```
+
+## Integration intent
+
+This update is source-level integration, not a separate add-on utility. The parser lives in the shipped `inc` tree and is imported by `ParseUtil.inc`, which also makes it available transitively to `SysUtil.inc` users. It can be treated like the rest of the built-in utility layer.
diff -ruN oxygen_repo/OxygenBasic_reviewed/demos/DataProcessing/JSONparsing.o2bas oxygen_work/demos/DataProcessing/JSONparsing.o2bas
--- oxygen_repo/OxygenBasic_reviewed/demos/DataProcessing/JSONparsing.o2bas	2025-02-13 13:25:08.000000000 +0000
+++ oxygen_work/demos/DataProcessing/JSONparsing.o2bas	2026-04-15 11:24:16.399141252 +0000
@@ -1,317 +1,48 @@
-'JSON PARSING
-'10/02/2025
-'CP
-
-uses console
-'
-'INPUT
-'
-string s=quote
-~~~
-
-{
-"employees":[
-    {"firstName":"John", "lastName":"Doe", value:30.5 },
-    {"firstName":"Anna", "lastName":"Smith"},
-    {"firstName":"Peter", "lastName":"Jones"}
-]
-}
-
-~~~
-
-interface
-=========
-'
-'s source strung
-'
-'outputs:
-'
-string name[1000] 'names of objects
-string  dat[1000] 'data of objects
-int    dmap[1000] 'location of name:object
-int    dlvl[1000] 'nesting level of objects
-int    je         'count of all objects
-'
-====================
-subroutine JsonParse
-====================
-'
-indexbase 1 'arrays lbound
-int i,j
-int gf,bf,be,cc,sc,db,ty,dm
-int qc,qf,qe,td,tf,te,sa,sd,nc
-byte*b
-@b=strptr(s)-1 'byte overlay
-i=0            'char iterator
-j=0            'index items
-sa=0           'array capture flag
-sd=0           'array items flag
-nc=0           'nesting depth
-'
-'
-===================================
-NextItem: 'clear flags and counters
-===================================
-'
-j++            'increment name / dat index
-qf=0           'quote flag start
-qe=0           'quote end
-qc=0           'quote toggle
-tf=0           'text flag
-bf=0           'object flag
-gf=0           'array fkag stt
-cc=0           '{} object depth
-sc=0           '[] array depth
-td=0           'end of text flag
-ty=0           'type of direct data
-dm=0           'dmap assigned flag
-ty=0           'raw data flgs
-db=sa          'data flag
-
-================
-do 'parsing loop
-================
-'
-@b++
-i++
-td=0 'assume delimiting char
-'
-select b 'byte pointer
-'
-=========
-case   0 ' ' end of string
-=========
-  '
-  exit do
-  '
-=========
-case 1 to 32 ' ' white space
-=========
-  '
-  continue do
-  '
-=========
-case  "\" 'escape char
-=========
-  @b++ : i++ : td=1
-  'itr edit out '\'
-  if b=0 then exit do
-=========
-case  34 '"' quote flag toggle
-=========
-  qc xor=1
-  if qc then
-    qf=i
-  else
-    qe=i
-    if gf+bf=0
-      if db
-        dat(j)=mid(s,qf+1,qe-qf-1)
-        if dm=0
-          dmap[j]=qf+1
-          dlvl[j]=nc
-        endif
-        goto nextitem
-      else
-        name(j)=mid(s,qf+1,qe-qf-1)
-        dmap[j]=qf+1
-        dlvl[j]=nc
-        dm=1
-        tf=0 'text flag
-        db=1 'data flag (expecting)
-        continue do
-      endif
-    endif
-  endif
-==========
-case qc<>0 
-==========
-  '
-  continue do 'till the quote ends
-  '
-=========
-case ","
-=========
-  'capture unquoted data
-  gosub captData
-  if ty
-    goto nextitem
-  endif
-=========
-case  ":"
-=========
-  if gf+bf=0
-    if qf=0 then 
-      name[j]=mid(s,tf,te-tf+1) 'raw name
-      dmap[j]=tf
-      dlvl[j]=nc
-      dm=1
-    endif
-    qf=0 'clear quote flag
-    tf=0 'text flag
-    db=1 'data flag (expecting)
-    continue do
-  endif
-=========
-case  "["
-=========
-  nc++
-  if db
-    sc++
-    if gf+bf=0
-      if sc=1 'start array
-        bf=i
-        sa=1 'array capture flag
-      endif
-    endif
-  else 'db=0
-    'splitting array into separate items
-    'no name assigned
-    name(j)=""
-    sd=1
-  endif
-=========
-case  "]"
-=========
-  nc--
-  sc--
-  gosub CaptData
-  if sc=0 'end inner array
-    sa=0
-    if ty=0
-      dat[j]=mid(s,bf,i-bf+1)   'brackets included
-      if dm=0
-        dmap[j]=bf
-        dlvl[j]=nc
-      endif
-    endif
-    goto nextitem
-  elseif sc<0
-    sc=0
-    sa=0
-    sd=0
-    if nc<=0
-      if ty
-        j++
-      endif
-      exit do 'splitting array complete
-    else
-      goto nextitem
-    endif
-  endif
-=========
-case "{"
-=========
-  nc++
-  if sa+sd
-    db=1 'objects inside an array
-  endif
-  if db
-    cc++
-    if cc=1 'inner object
-      gf=i  'start of {} block
-    endif
-  else 'db=0
-    'splitting an object into parts
-  endif
-=========
-case "}"
-=========
-  nc--
-  cc--
-  gosub CaptData
-  if cc=0 'end inner object
-    if sa=0 'not inside array
-      dat[j]=mid(s,gf,i-gf+1) 'brackets included
-      if dm=0
-        dmap[j]=gf
-        dlvl[j]=nc
-      endif
-      goto nextitem
-    endif
-  elseif cc<0
-    cc=0
-    if nc<=0
-      if ty
-        j++
-      endif
-      exit do 'splitting array complete
-    else
-      goto nextitem
-    endif
-  endif
-=========
-case else 'covers all other ascii
-=========
-  td=1
-  if gf+bf=0
-    if tf=0
-      tf=i 
-      'start of unquoted /
-      'name/number/true/flse/null
-    endif
-    te=i
-  endif
-end select
-'
-'
-end do 'parsing loop
-
-
-SubRoutine CaptData 'internal
-=============================
-'
-ty=0
-if gf+bf+td+qf=0
-  if tf
-    if db
-      'unquoted data
-      dat[j]=mid(s,tf,te-tf+1)
-      if dm=0
-        dmap[j]=tf
-        dlvl[j]=nc
-      endif
-      'numbers/true/falsr/null
-      ty=1
-    endif
-  endif
-endif
-end subroutine 'CaptData
-
-
-je=j-1 'set end of data
-
-end subroutine 'JsonParse
-'
-'===========
-'TEST OUTPUT
-'===========
-'
-int i
-print cr
-gosub JsonParse
-for i=1 to je
-  print i " NAME: " name[i] cr
-  print i " DATA: " dat[i] cr
-next
-print cr cr
-'
-print "split array" cr
-s=dat(1)
-gosub JsonParse
-for i=1 to je
-  print i " NAME: " name[i] cr
-  print i " DATA: " dat[i] cr
-next
-print cr
-
-print "split object" cr
-s=dat(1)
-gosub JsonParse
-for i=1 to je
-  print i " NAME: " name[i] cr
-  print i " DATA: " dat[i] cr
-next
-print cr
-
-wait(100000)
+'JSON PARSING
+'15/04/2026
+'System JsonUtil demo
+
+uses console
+uses JsonUtil
+
+string s=quote
+~~~
+{
+  "employees":[
+    {"firstName":"John",  "lastName":"Doe",   "value":30.5},
+    {"firstName":"Anna",  "lastName":"Smith"},
+    {"firstName":"Peter", "lastName":"Jones"}
+  ],
+  "active":true
+}
+~~~
+
+new JsonDoc jd
+int ok=jd.Parse(s)
+
+if ok=0
+  print "Parse error at " jd.ErrorPos " : " jd.ErrorText cr
+else
+  int employees
+  int row
+  int fn
+  int ln
+  int i
+  
+  print "Node count : " jd.NodeCount cr
+  print "Root kind  : " jd.NodeTypeName(jd.RootId) cr cr
+
+  employees=jd.Query("$.employees")
+  for i=0 to jd.ItemCount(employees)-1
+    row=jd.Item(employees,i)
+    fn=jd.FindChild(row,"firstName")
+    ln=jd.FindChild(row,"lastName")
+    print "employee[" str(i) "] = " jd.NodeValue(fn) " " jd.NodeValue(ln) cr
+  next
+
+  print cr "active = " jd.NodeValue(jd.Query("$.active")) cr cr
+  print jd.Stringify(1) cr
+endif
+
+del jd
+wait
diff -ruN oxygen_repo/OxygenBasic_reviewed/inc/JsonUtil.inc oxygen_work/inc/JsonUtil.inc
--- oxygen_repo/OxygenBasic_reviewed/inc/JsonUtil.inc	1970-01-01 00:00:00.000000000 +0000
+++ oxygen_work/inc/JsonUtil.inc	2026-04-15 11:26:51.528009319 +0000
@@ -0,0 +1,828 @@
+  'JSONUTIL.INC
+  '
+  'DOM style JSON parser and formatter for OxygenBasic.
+  'Adds a reusable system module based on the ad-hoc demo parser,
+  'but with dynamic storage, stable navigation helpers, and round-trip
+  'formatting support.
+  '
+  '15/04/2026
+  '
+  % JSON_INVALID 0
+  % JSON_OBJECT  1
+  % JSON_ARRAY   2
+  % JSON_STRING  3
+  % JSON_NUMBER  4
+  % JSON_TRUE    5
+  % JSON_FALSE   6
+  % JSON_NULL    7
+
+
+  function JsonHexNibble(int c) as int
+  ====================================
+  select c
+    case 48 to 57   : return c-48
+    case 65 to 70   : return c-55
+    case 97 to 102  : return c-87
+  end select
+  return -1
+  end function
+
+
+  function JsonKindName(int k) as string
+  ======================================
+  select k
+    case JSON_OBJECT : return "object"
+    case JSON_ARRAY  : return "array"
+    case JSON_STRING : return "string"
+    case JSON_NUMBER : return "number"
+    case JSON_TRUE   : return "true"
+    case JSON_FALSE  : return "false"
+    case JSON_NULL   : return "null"
+  end select
+  return "invalid"
+  end function
+
+
+  function JsonEscape(string s) as string
+  =======================================
+  int i,ls,a
+  string o
+  string hx
+  ls=len s
+  if not ls then return ""
+  byte b at strptr s
+  for i=1 to ls
+    a=b[i]
+    select a
+      case 34  : o+=chr(92,34)
+      case 92  : o+=chr(92,92)
+      case 8   : o+=chr(92,98)
+      case 9   : o+=chr(92,116)
+      case 10  : o+=chr(92,110)
+      case 12  : o+=chr(92,102)
+      case 13  : o+=chr(92,114)
+      case 0 to 31
+        hx=mid(hex(a,2),-2)
+        o+=chr(92,117,48,48)+hx
+      case else
+        o+=chr(a)
+    end select
+  next
+  return o
+  end function
+
+
+  function JsonQuote(string s) as string
+  ======================================
+  return chr(34)+JsonEscape(s)+chr(34)
+  end function
+
+
+  function JsonIsDigits(string s) as int
+  ======================================
+  int i,ls,a
+  ls=len s
+  if not ls then return 0
+  byte b at strptr s
+  for i=1 to ls
+    a=b[i]
+    if a<48 then return 0
+    if a>57 then return 0
+  next
+  return 1
+  end function
+
+
+  type JsonNode
+  =============
+    bstring key
+    bstring dat
+    int kind
+    int parent
+    int first
+    int last
+    int next
+    int count
+    int level
+    int pos
+    int stop
+  end type
+
+
+  class JsonDoc
+  =============
+
+    indexbase 1
+
+    JsonNode* nd
+    int cap
+    int cnt
+    int root
+    int errpos
+    bstring errmsg
+    bstring src
+    bstring obuf
+    int opos
+    int olen
+
+
+    method constructor(int n=256)
+    =============================
+    if n<32 then n=32
+    cap=n
+    @nd=getmemory sizeof(JsonNode)*(cap+1)
+    end method
+
+
+    method destructor()
+    ===================
+    clear()
+    end method
+
+
+    method clear()
+    ==============
+    int i
+    if @nd
+      for i=1 to cnt
+        del nd[i].key
+        del nd[i].dat
+      next
+      freememory @nd
+    endif
+    @nd=0
+    cap=0
+    cnt=0
+    root=0
+    errpos=0
+    del errmsg
+    del src
+    del obuf
+    opos=0
+    olen=0
+    end method
+
+
+    method ResetForParse(int n=256)
+    ===============================
+    clear()
+    if n<32 then n=32
+    cap=n
+    @nd=getmemory sizeof(JsonNode)*(cap+1)
+    end method
+
+
+    method reserve(int n)
+    =====================
+    sys old
+    int oldcap
+    if n<=cap then exit method
+    old=@nd
+    oldcap=cap
+    cap=n
+    @nd=getmemory sizeof(JsonNode)*(cap+1)
+    if old
+      copy @nd,old,sizeof(JsonNode)*(oldcap+1)
+      freememory old
+    endif
+    end method
+
+
+    method SetError(int p, string s)
+    ================================
+    if errpos then exit method
+    errpos=p
+    errmsg=s
+    end method
+
+
+    method SkipSpace(int*i)
+    =======================
+    byte b at strptr src
+    do
+      select b[i]
+        case 9,10,13,32 : i++
+        case else       : exit do
+      end select
+    loop
+    end method
+
+
+    method AppendNode(int parent,kind,string key="",dat="", int pos=0,stop=0,level=0) as int
+    ===========================================================================================
+    cnt++
+    if cnt>cap then reserve(cap+cap+32)
+    nd[cnt]={key,dat,kind,parent,0,0,0,0,level,pos,stop}
+    if parent>0
+      if nd[parent].first=0
+        nd[parent].first=cnt
+      else
+        nd[nd[parent].last].next=cnt
+      endif
+      nd[parent].last=cnt
+      nd[parent].count++
+    else
+      root=cnt
+    endif
+    return cnt
+    end method
+
+
+    method ParseStringAt(string s, int*i) as string
+    ===============================================
+    int ls,a,q,cp,j
+    int hx1,hx2,hx3,hx4,uc
+    int seg
+    string out,hx
+    byte b at strptr s
+    ls=len s
+    if b[i]<>34
+      SetError i,"String expected"
+      return ""
+    endif
+    q=i
+    i++
+    seg=i
+    do
+      a=b[i]
+      select a
+        case 0
+          SetError q,"Unterminated string"
+          return ""
+        case 34
+          if seg<i then out+=mid(s,seg,i-seg)
+          i++
+          return out
+        case 92
+          if seg<i then out+=mid(s,seg,i-seg)
+          i++
+          select b[i]
+            case 34  : out+=chr(34)
+            case 92  : out+=chr(92)
+            case 47  : out+=chr(47)
+            case 98  : out+=chr(8)
+            case 102 : out+=chr(12)
+            case 110 : out+=chr(10)
+            case 114 : out+=chr(13)
+            case 116 : out+=chr(9)
+            case 117
+              if i+4>ls
+                SetError i,"Short unicode escape"
+                return out
+              endif
+              hx1=JsonHexNibble(b[i+1])
+              hx2=JsonHexNibble(b[i+2])
+              hx3=JsonHexNibble(b[i+3])
+              hx4=JsonHexNibble(b[i+4])
+              if hx1<0 or hx2<0 or hx3<0 or hx4<0
+                SetError i,"Invalid unicode escape"
+                return out
+              endif
+              uc=((hx1*16+hx2)*16+hx3)*16+hx4
+              if uc<=255
+                out+=chr(uc)
+              else
+                hx=mid(s,i+1,4)
+                out+=chr(92,117)+hx
+              endif
+              i+=4
+            case else
+              SetError i,"Invalid string escape"
+              return out
+          end select
+          i++
+          seg=i
+          continue do
+        case 1 to 31
+          SetError i,"Control character inside string"
+          return out
+      end select
+      i++
+    loop
+    end method
+
+
+    method ParseNumberAt(int*i) as string
+    =====================================
+    int a,ls
+    byte b at strptr src
+    ls=len src
+    a=i
+    if b[i]=45 then i++
+    select b[i]
+      case 48
+        i++
+      case 49 to 57
+        do
+          i++
+          if b[i]<48 then exit do
+          if b[i]>57 then exit do
+        loop
+      case else
+        SetError i,"Invalid number"
+        return ""
+    end select
+    if b[i]=46
+      i++
+      if b[i]<48 or b[i]>57
+        SetError i,"Fraction digits expected"
+        return ""
+      endif
+      do
+        i++
+        if b[i]<48 then exit do
+        if b[i]>57 then exit do
+      loop
+    endif
+    if b[i]=69 or b[i]=101
+      i++
+      if b[i]=43 or b[i]=45 then i++
+      if b[i]<48 or b[i]>57
+        SetError i,"Exponent digits expected"
+        return ""
+      endif
+      do
+        i++
+        if b[i]<48 then exit do
+        if b[i]>57 then exit do
+      loop
+    endif
+    return mid(src,a,i-a)
+    end method
+
+
+    method ParseValue(int*i, int parent=0, string key="", int level=0) as int
+    ================================================================
+    int node,start
+    string dat
+    byte b at strptr src
+    SkipSpace i
+    start=i
+    select b[i]
+      case 123
+        return ParseObject(i,parent,key,level)
+      case 91
+        return ParseArray(i,parent,key,level)
+      case 34
+        dat=ParseStringAt(src,i)
+        if errpos then return 0
+        node=AppendNode(parent,JSON_STRING,key,dat,start,i-1,level)
+        return node
+      case 45,48 to 57
+        dat=ParseNumberAt(i)
+        if errpos then return 0
+        node=AppendNode(parent,JSON_NUMBER,key,dat,start,i-1,level)
+        return node
+      case 116
+        if mid(src,i,4)<>"true"
+          SetError i,"Unknown token"
+          return 0
+        endif
+        i+=4
+        node=AppendNode(parent,JSON_TRUE,key,"true",start,i-1,level)
+        return node
+      case 102
+        if mid(src,i,5)<>"false"
+          SetError i,"Unknown token"
+          return 0
+        endif
+        i+=5
+        node=AppendNode(parent,JSON_FALSE,key,"false",start,i-1,level)
+        return node
+      case 110
+        if mid(src,i,4)<>"null"
+          SetError i,"Unknown token"
+          return 0
+        endif
+        i+=4
+        node=AppendNode(parent,JSON_NULL,key,"null",start,i-1,level)
+        return node
+    end select
+    SetError i,"Value expected"
+    return 0
+    end method
+
+
+    method ParseObject(int*i, int parent=0, string key="", int level=0) as int
+    ==================================================================
+    int node,start
+    string k
+    byte b at strptr src
+    start=i
+    i++
+    node=AppendNode(parent,JSON_OBJECT,key,"",start,0,level)
+    SkipSpace i
+    if b[i]=125
+      i++
+      nd[node].stop=i-1
+      return node
+    endif
+    do
+      SkipSpace i
+      if b[i]<>34
+        SetError i,"Object key expected"
+        return 0
+      endif
+      k=ParseStringAt(src,i)
+      if errpos then return 0
+      SkipSpace i
+      if b[i]<>58
+        SetError i,"Colon expected"
+        return 0
+      endif
+      i++
+      if ParseValue(i,node,k,level+1)=0
+        return 0
+      endif
+      if errpos then return 0
+      SkipSpace i
+      select b[i]
+        case 44 : i++
+        case 125
+          i++
+          nd[node].stop=i-1
+          return node
+        case else
+          SetError i,"Comma or } expected"
+          return 0
+      end select
+    loop
+    end method
+
+
+    method ParseArray(int*i, int parent=0, string key="", int level=0) as int
+    ================================================================
+    int node,start
+    byte b at strptr src
+    start=i
+    i++
+    node=AppendNode(parent,JSON_ARRAY,key,"",start,0,level)
+    SkipSpace i
+    if b[i]=93
+      i++
+      nd[node].stop=i-1
+      return node
+    endif
+    do
+      if ParseValue(i,node,"",level+1)=0
+        return 0
+      endif
+      if errpos then return 0
+      SkipSpace i
+      select b[i]
+        case 44 : i++
+        case 93
+          i++
+          nd[node].stop=i-1
+          return node
+        case else
+          SetError i,"Comma or ] expected"
+          return 0
+      end select
+    loop
+    end method
+
+
+    method Parse(string s) as int
+    =============================
+    int i=1
+    int n=cap
+    if n<32 then n=256
+    ResetForParse n
+    src=s
+    SkipSpace i
+    if ParseValue(i,0,"",0)=0
+      return 0
+    endif
+    if errpos then return 0
+    SkipSpace i
+    if i<=len(src)
+      SetError i,"Trailing data after root"
+      return 0
+    endif
+    return root
+    end method
+
+
+    method Valid() as int
+    =====================
+    if errpos then return 0
+    if root=0 then return 0
+    return 1
+    end method
+
+
+    method ErrorText() as string
+    ============================
+    return errmsg
+    end method
+
+
+    method ErrorPos() as int
+    ========================
+    return errpos
+    end method
+
+
+    method RootId() as int
+    ======================
+    return root
+    end method
+
+
+    method NodeCount() as int
+    =========================
+    return cnt
+    end method
+
+
+    method NodeKind(int n) as int
+    =============================
+    if n<1 or n>cnt then return JSON_INVALID
+    return nd[n].kind
+    end method
+
+
+    method NodeTypeName(int n) as string
+    ====================================
+    if n<1 or n>cnt then return "invalid"
+    return JsonKindName(nd[n].kind)
+    end method
+
+
+    method NodeKey(int n) as string
+    ===============================
+    if n<1 or n>cnt then return ""
+    return nd[n].key
+    end method
+
+
+    method NodeValue(int n) as string
+    =================================
+    if n<1 or n>cnt then return ""
+    return nd[n].dat
+    end method
+
+
+    method ParentId(int n) as int
+    =============================
+    if n<1 or n>cnt then return 0
+    return nd[n].parent
+    end method
+
+
+    method NodeLevel(int n) as int
+    ==============================
+    if n<1 or n>cnt then return 0
+    return nd[n].level
+    end method
+
+
+    method NodePos(int n) as int
+    ============================
+    if n<1 or n>cnt then return 0
+    return nd[n].pos
+    end method
+
+
+    method NodeStop(int n) as int
+    =============================
+    if n<1 or n>cnt then return 0
+    return nd[n].stop
+    end method
+
+
+    method FirstChild(int n) as int
+    ===============================
+    if n<1 or n>cnt then return 0
+    return nd[n].first
+    end method
+
+
+    method NextSibling(int n) as int
+    =================================
+    if n<1 or n>cnt then return 0
+    return nd[n].next
+    end method
+
+
+    method ItemCount(int n) as int
+    ==============================
+    if n<1 or n>cnt then return 0
+    return nd[n].count
+    end method
+
+
+    method FindChild(int parent, string key) as int
+    ===============================================
+    int n
+    if parent<1 or parent>cnt then return 0
+    n=nd[parent].first
+    while n
+      if nd[n].key=key then return n
+      n=nd[n].next
+    wend
+    return 0
+    end method
+
+
+    method Item(int parent,index) as int
+    ====================================
+    int n
+    if parent<1 or parent>cnt then return 0
+    if index<0 then return 0
+    n=nd[parent].first
+    while n
+      if index=0 then return n
+      index-=1
+      n=nd[n].next
+    wend
+    return 0
+    end method
+
+
+    method Query(string path, int start=0) as int
+    =============================================
+    int i,ls,a
+    int n
+    string seg
+    byte b at strptr path
+    ls=len path
+    if start>0 then n=start else n=root
+    if not n then return 0
+    if ls=0 then return n
+    i=1
+    if b[i]=36
+      n=root
+      i++
+    endif
+    do
+      if i>ls then exit do
+      select b[i]
+        case 46
+          i++
+          if i>ls then exit do
+          a=i
+          do
+            if i>ls then exit do
+            if b[i]=46 or b[i]=91 then exit do
+            i++
+          loop
+          seg=mid(path,a,i-a)
+          if not seg then return 0
+          n=FindChild(n,seg)
+          if not n then return 0
+        case 91
+          i++
+          a=i
+          do
+            if i>ls then return 0
+            if b[i]=93 then exit do
+            i++
+          loop
+          seg=mid(path,a,i-a)
+          i++
+          if not seg then return 0
+          if JsonIsDigits(seg) and nd[n].kind=JSON_ARRAY
+            n=Item(n,val(seg))
+          else
+            n=FindChild(n,seg)
+          endif
+          if not n then return 0
+        case else
+          a=i
+          do
+            if i>ls then exit do
+            if b[i]=46 or b[i]=91 then exit do
+            i++
+          loop
+          seg=mid(path,a,i-a)
+          if not seg then return 0
+          n=FindChild(n,seg)
+          if not n then return 0
+      end select
+    loop
+    return n
+    end method
+
+
+    method OutAppend(string s)
+    ==========================
+    int ls
+    ls=len s
+    if ls=0 then exit method
+    if opos+ls>olen
+      obuf+=nuls 0x4000+ls*2
+      olen=len obuf
+    endif
+    mid obuf,opos+1,s
+    opos+=ls
+    end method
+
+
+    method OutIndent(int depth)
+    ===========================
+    if depth>0 then OutAppend space(depth*2)
+    end method
+
+
+    method WriteNode(int n, int pretty=0, depth=0)
+    ===============================================
+    int c,first=1
+    select nd[n].kind
+      case JSON_OBJECT
+        OutAppend "{"
+        c=nd[n].first
+        if c
+          if pretty then OutAppend chr(13,10)
+          while c
+            if not first
+              OutAppend ","
+              if pretty then OutAppend chr(13,10)
+            endif
+            if pretty then OutIndent depth+1
+            OutAppend JsonQuote(nd[c].key)
+            if pretty then
+              OutAppend ": "
+            else
+              OutAppend ":"
+            endif
+            WriteNode c,pretty,depth+1
+            first=0
+            c=nd[c].next
+          wend
+          if pretty then
+            OutAppend chr(13,10)
+            OutIndent depth
+          endif
+        endif
+        OutAppend "}"
+      case JSON_ARRAY
+        OutAppend "["
+        c=nd[n].first
+        if c
+          if pretty then OutAppend chr(13,10)
+          while c
+            if not first
+              OutAppend ","
+              if pretty then OutAppend chr(13,10)
+            endif
+            if pretty then OutIndent depth+1
+            WriteNode c,pretty,depth+1
+            first=0
+            c=nd[c].next
+          wend
+          if pretty then
+            OutAppend chr(13,10)
+            OutIndent depth
+          endif
+        endif
+        OutAppend "]"
+      case JSON_STRING
+        OutAppend JsonQuote(nd[n].dat)
+      case else
+        OutAppend nd[n].dat
+    end select
+    end method
+
+
+    method Stringify(int pretty=0) as string
+    ========================================
+    if not root then return ""
+    opos=0
+    if len(obuf)=0
+      obuf=nuls 0x4000
+      olen=len obuf
+    endif
+    WriteNode root,pretty,0
+    return left(obuf,opos)
+    end method
+
+  end class
+
+
+  function JsonFormat(string s, int pretty=1) as string
+  =====================================================
+  new JsonDoc jd
+  if jd.Parse(s)=0
+    del jd
+    return ""
+  endif
+  function=jd.Stringify(pretty)
+  del jd
+  end function
+
+
+  function JsonMinify(string s) as string
+  =======================================
+  return JsonFormat(s,0)
+  end function
+
+
+  function JsonValid(string s) as int
+  ===================================
+  new JsonDoc jd
+  function=jd.Parse(s)
+  if function then function=1
+  del jd
+  end function
diff -ruN oxygen_repo/OxygenBasic_reviewed/inc/ParseUtil.inc oxygen_work/inc/ParseUtil.inc
--- oxygen_repo/OxygenBasic_reviewed/inc/ParseUtil.inc	2023-12-15 07:34:34.000000000 +0000
+++ oxygen_work/inc/ParseUtil.inc	2026-04-15 11:30:04.259594804 +0000
@@ -6,6 +6,7 @@
 
 
 uses StringUtil
+uses JsonUtil  ' DOM-style JSON parser / formatter
 
 'parseutil state varables used by getword etc
 =============================================
