Json.parse decodes an escaped string in one pass into one buffer - joined a character at a time, every shorter copy was kept, and a long escaped text took gigabytes (ui-preview: 6.5 GB in 3 s); protocol-v1.md states the @import path rule and the key a file override answers to
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ea51307eaa
commit
4fb1dc0ddc
3 changed files with 64 additions and 30 deletions
6
changes/json-escape-linear.md
Normal file
6
changes/json-escape-linear.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`Json.parse` decodes an escaped string in one pass.** A string with escapes in it was joined a character
|
||||||
|
at a time, and every shorter copy was kept: a long escaped text took gigabytes (ui-preview's story model
|
||||||
|
reached 6.5 GB in three seconds). It is now decoded into one buffer sized from the text. ui-preview's
|
||||||
|
protocol-v1.md states how an `@import` path is resolved and which key a `file` override answers to.
|
||||||
|
|
@ -605,36 +605,67 @@ function jp_string(p: JP) -> string {
|
||||||
return p.s[a..p.n]
|
return p.s[a..p.n]
|
||||||
}
|
}
|
||||||
|
|
||||||
# the same with escapes in it, the rare case, a character at a time
|
# the same with escapes in it: decoded in one pass into one buffer, sized by the text up to the closing
|
||||||
|
# quote: an escape decodes to at most one and a half times its bytes (\uXXXX is six bytes and at most
|
||||||
|
# three out; a \u with bad hex is U+FFFD, three for two).
|
||||||
|
# Joining the result a character at a time kept every shorter copy - a long escaped text made
|
||||||
|
# gigabytes of them (ui-preview's model, 6.5 GB in three seconds).
|
||||||
function jp_string_esc(p: JP) -> string {
|
function jp_string_esc(p: JP) -> string {
|
||||||
var out = ""
|
var end = p.i
|
||||||
|
while end < p.n and p.s[end] != '"' {
|
||||||
|
if p.s[end] == '\\' { end += 1 }
|
||||||
|
end += 1
|
||||||
|
}
|
||||||
|
if end > p.n { end = p.n }
|
||||||
|
let out = bytes((end - p.i) * 3 / 2 + 4) # a \u with bad hex is U+FFFD: three bytes for two
|
||||||
|
var w = 0
|
||||||
while p.i < p.n {
|
while p.i < p.n {
|
||||||
let c = p.s[p.i]
|
let c = p.s[p.i]
|
||||||
if c == '"' { p.i += 1; return out } # closing quote
|
if c == '"' { # closing quote
|
||||||
|
p.i += 1
|
||||||
|
out[w] = 0
|
||||||
|
let done: string = out
|
||||||
|
return done
|
||||||
|
}
|
||||||
if c == '\\' { # escape
|
if c == '\\' { # escape
|
||||||
p.i += 1
|
p.i += 1
|
||||||
if p.i < p.n {
|
if p.i < p.n {
|
||||||
let e = p.s[p.i]
|
let e = p.s[p.i]
|
||||||
if e == 'n' { out += "\n" }
|
if e == 'n' { out[w] = 10; w += 1 }
|
||||||
else if e == 't' { out += "\t" }
|
else if e == 't' { out[w] = 9; w += 1 }
|
||||||
else if e == 'r' { out += "\r" }
|
else if e == 'r' { out[w] = 13; w += 1 }
|
||||||
else if e == 'b' or e == 'f' or e == 'u' {
|
else if e == 'b' { out[w] = 8; w += 1 }
|
||||||
var code = 8
|
else if e == 'f' { out[w] = 12; w += 1 }
|
||||||
if e == 'f' { code = 12 }
|
else if e == 'u' { w = jp_put_utf8(out, w, jp_code(p)) } # leaves p.i on the escape's last hex digit
|
||||||
if e == 'u' { code = jp_code(p) } # leaves p.i on the escape's last hex digit
|
else { out[w] = e; w += 1 } # \" \\ \/ -> the literal char
|
||||||
let u = jp_utf8(code)
|
|
||||||
out += u
|
|
||||||
free(u)
|
|
||||||
}
|
|
||||||
else { out += p.s[p.i..p.i + 1] } # \" \\ \/ -> the literal char
|
|
||||||
p.i += 1
|
p.i += 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
out += p.s[p.i..p.i + 1]
|
out[w] = c
|
||||||
|
w += 1
|
||||||
p.i += 1
|
p.i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
out[w] = 0
|
||||||
|
let rest: string = out
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
# one code point's UTF-8 bytes into out at w; where the next one goes
|
||||||
|
function jp_put_utf8(out: bytes, w: int, c: int) -> int {
|
||||||
|
if c < 128 {
|
||||||
|
out[w] = c
|
||||||
|
return w + 1
|
||||||
|
}
|
||||||
|
if c < 2048 {
|
||||||
|
out[w] = 192 | (c >> 6); out[w + 1] = 128 | (c & 63)
|
||||||
|
return w + 2
|
||||||
|
}
|
||||||
|
if c < 65536 {
|
||||||
|
out[w] = 224 | (c >> 12); out[w + 1] = 128 | ((c >> 6) & 63); out[w + 2] = 128 | (c & 63)
|
||||||
|
return w + 3
|
||||||
|
}
|
||||||
|
out[w] = 240 | (c >> 18); out[w + 1] = 128 | ((c >> 12) & 63); out[w + 2] = 128 | ((c >> 6) & 63); out[w + 3] = 128 | (c & 63)
|
||||||
|
return w + 4
|
||||||
}
|
}
|
||||||
|
|
||||||
# \uXXXX, p.i on the 'u': the code point, a surrogate pair joined (\ud83d\ude00 is one emoji); a
|
# \uXXXX, p.i on the 'u': the code point, a surrogate pair joined (\ud83d\ude00 is one emoji); a
|
||||||
|
|
@ -671,18 +702,6 @@ function jp_hex4(p: JP, at: int) -> int {
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
# one code point as a UTF-8 string
|
|
||||||
function jp_utf8(c: int) -> string {
|
|
||||||
let out = bytes(5)
|
|
||||||
var n = 0
|
|
||||||
if c < 128 { out[0] = c; n = 1 }
|
|
||||||
else if c < 2048 { out[0] = 192 | (c >> 6); out[1] = 128 | (c & 63); n = 2 }
|
|
||||||
else if c < 65536 { out[0] = 224 | (c >> 12); out[1] = 128 | ((c >> 6) & 63); out[2] = 128 | (c & 63); n = 3 }
|
|
||||||
else { out[0] = 240 | (c >> 18); out[1] = 128 | ((c >> 12) & 63); out[2] = 128 | ((c >> 6) & 63); out[3] = 128 | (c & 63); n = 4 }
|
|
||||||
out[n] = 0
|
|
||||||
let t: string = out
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
# read a number; a '.' makes it a fixed node, otherwise an int node.
|
# read a number; a '.' makes it a fixed node, otherwise an int node.
|
||||||
function jp_number(p: JP) -> Val {
|
function jp_number(p: JP) -> Val {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ A template error is NOT a `fail`: the request succeeded and the error is reporte
|
||||||
| `font <text dir>` | none. Reads `<dir>/font.json` (§5.3); `fail` if it has no `adv` list |
|
| `font <text dir>` | none. Reads `<dir>/font.json` (§5.3); `fail` if it has no `adv` list |
|
||||||
| `viewport <num w> <num h> [<num scale>]` | none. The frame's size in screen pixels, and screen pixels a design pixel; with no scale, `h / 1080` (the game's own rule). Default 1920 1080 |
|
| `viewport <num w> <num h> [<num scale>]` | none. The frame's size in screen pixels, and screen pixels a design pixel; with no scale, `h / 1080` (the game's own rule). Default 1920 1080 |
|
||||||
| `locale <text po path>` | none. Every text a template shows is looked up in that gettext file as the game looks it up (ludic.i18n: exact, then patterns with holes, then sentence by sentence). `""` or `en` is English. The path is read through `file` overrides first |
|
| `locale <text po path>` | none. Every text a template shows is looked up in that gettext file as the game looks it up (ludic.i18n: exact, then patterns with holes, then sentence by sentence). `""` or `en` is English. The path is read through `file` overrides first |
|
||||||
| `load <text Name> <text xml> <text lss> [<text xml path> [<text lss path>]]` | events only (`err`). Registers component class `Name`, or replaces its template and styles in place if it exists. Paths default to `Name.xml` and `Name.lss`; they name the files in errors and rules, and an `@import` in the styles is read from beside the xml path. Every capitalised tag in the template naming a class not yet loaded gets an empty class (`<row/>`) of that name, filled in place when the studio loads it; the studio sends every class a story names before it `show`s the root |
|
| `load <text Name> <text xml> <text lss> [<text xml path> [<text lss path>]]` | events only (`err`). Registers component class `Name`, or replaces its template and styles in place if it exists. Paths default to `Name.xml` and `Name.lss`; they name the files in errors and rules, and an `@import` in the styles is resolved beside the lss path (§4.1). Every capitalised tag in the template naming a class not yet loaded gets an empty class (`<row/>`) of that name, filled in place when the studio loads it; the studio sends every class a story names before it `show`s the root |
|
||||||
| `model <text Name> <json object>` | none. The class's model: every key is a name its template reads. Props from the parent's tag and values the template `set`s are laid over it |
|
| `model <text Name> <json object>` | none. The class's model: every key is a name its template reads. Props from the parent's tag and values the template `set`s are laid over it |
|
||||||
| `calls <text Name> <json object>` | none. How the class's functions answer (§7) |
|
| `calls <text Name> <json object>` | none. How the class's functions answer (§7) |
|
||||||
| `show <text Name>` | none. The class a `frame` shows, as a whole screen; `fail` if it is not loaded |
|
| `show <text Name>` | none. The class a `frame` shows, as a whole screen; `fail` if it is not loaded |
|
||||||
|
|
@ -87,6 +87,15 @@ A template error is NOT a `fail`: the request succeeded and the error is reporte
|
||||||
|
|
||||||
Input takes effect in the next `frame`; a press's events are reported by that frame.
|
Input takes effect in the next `frame`; a press's events are reported by that frame.
|
||||||
|
|
||||||
|
### 4.1 Paths
|
||||||
|
|
||||||
|
This is the rule v1 keeps. A later version will not change it silently.
|
||||||
|
|
||||||
|
- **The rule is for `@import` only.** A `locale` path and an `image` source are read exactly as written, relative to the host's working directory, and a `file` override for either uses that same text.
|
||||||
|
- **A leading `/` in an `@import`** means the game's root, which is the host's working directory. The slash is dropped: `@import "/src/ui/kit/theme.lss"` reads `src/ui/kit/theme.lss`. So an absolute filesystem path cannot be imported; start the host in the game's folder and write the path from there.
|
||||||
|
- **Any other `@import`** is joined to the directory of the sheet that imports it. For a class's own styles, that is its lss path as `load` gave it. For an imported sheet, it is that sheet's resolved path. So a sheet loaded as `src/ui/hud/Hud.lss` that imports `../kit/theme.lss` reads `src/ui/hud/../kit/theme.lss`, with no normalising of `..`. A class loaded under an absolute lss path imports under absolute paths.
|
||||||
|
- **A `file` override** applies to exactly the resolved path, byte for byte: `src/ui/kit/theme.lss` for the first example, and `src/ui/hud/../kit/theme.lss` for the second. Neither `/src/...` nor a normalised form matches.
|
||||||
|
|
||||||
## 5. The frame
|
## 5. The frame
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue