nupp.text.utf8
nupp.text.utf8 reads Unicode scalars out of strings and nupp.io.ByteView values. Reach for it to count, decode, validate or truncate text whose bytes came from somewhere else.
length counts scalars, replacing each malformed byte with one replacement scalar rather than refusing the string. encode goes the other way, from one scalar value to its bytes.
Byte offsets#
Offsets here are 1-based, so they compose with Lua string positions and with string.sub on the same text. This is the opposite of the zero-based offsets a buffer uses, because those are offsets into storage rather than positions in text.
Decoding a scalar#
decodeAt decodes forward from an offset and answers the scalar with the offset after it. decodeBefore decodes the scalar ending before an offset and answers it with the offset it starts at, so the two walk in opposite directions over the same positions:
const utf8 = nupp.text.utf8
local codepoint, nextByte = utf8.decodeAt("A€", 2)
assert(codepoint == 0x20ac and nextByte == 5)
codepoint, nextByte = utf8.decodeBefore("A€", nextByte)
assert(codepoint == 0x20ac and nextByte == 2)A malformed byte answers 0xFFFD and advances exactly one byte, so a walk over damaged text makes progress rather than stalling. Nil is the end of the value, and that is what stops the walk:
const utf8 = nupp.text.utf8
local at = 1
while true do
local codepoint, nextAt = utf8.decodeAt(text, at)
if codepoint == nil then break end
at = nextAt
endOne past the end is an accepted offset; anything further raises.
Validating and truncating#
Validation is explicit, because decoding never refuses. isValid checks a complete byte sequence:
Overlong forms, surrogate halves and codepoints above the maximum are malformed even though their lead bytes are well formed, so they are rejected on the value rather than on the shape.
validPrefixLength(value, maxBytes) answers the length of the largest valid prefix no longer than a byte budget, which is what keeps a fixed-width field from splitting a scalar in half. truncate applies that to a string and copies the prefix:
const utf8 = nupp.text.utf8
assert(utf8.validPrefixLength("A€B", 4) == 4)
assert(utf8.truncate("A€B", 4) == "A€")truncate takes a string, since it answers one. Everything else here takes a string or a byte view.
Module contents
Functions
| Function | Kind | Description |
|---|---|---|
decodeAt | function | Reads the codepoint starting at a byte offset, and where the next one starts. |
decodeBefore | function | Reads the codepoint ending just before a byte offset, and where it starts. |
encode | function | Encodes one codepoint as UTF-8 bytes. |
isValid | function | Reports whether the whole value is well-formed UTF-8. |
length | function | Counts the codepoints in the value. |
truncate | function | Cuts text to at most maxBytes, never through a codepoint. |
validPrefixLength | function | Measures the longest well-formed prefix no longer than maxBytes. |
Functions#
utf8.decodeAtfunction#
Reads the codepoint starting at a byte offset, and where the next one starts.
Walking a buffer is this call and nothing else, because a malformed byte answers 0xFFFD and still advances:
const utf8 = nupp.text.utf8
local at = 1
while true do
local codepoint, nextAt = utf8.decodeAt(text, at)
if codepoint == nil then break end
at = nextAt
endArguments
| Name | Type | Description |
|---|---|---|
value | string | ByteView | the bytes to read |
byteOffset | integer | the 1-based byte to start at; one past the end is allowed |
Returns
| Type | Description |
|---|---|
integer? | the codepoint, or nil at the end of the value |
integer | the byte the next codepoint starts at |
Raises
when byteOffset is not an integer, or is outside the value
utf8.decodeBeforefunction#
function utf8.decodeBefore(value: string | ByteView, byteOffset: integer): integer?, integerReads the codepoint ending just before a byte offset, and where it starts.
decodeAt and decodeBefore walk the same positions in opposite directions, so decoding forward and then back lands where it started:
const utf8 = nupp.text.utf8
local codepoint, nextByte = utf8.decodeAt("A€", 2)
assert(codepoint == 0x20ac and nextByte == 5)
codepoint, nextByte = utf8.decodeBefore("A€", nextByte)
assert(codepoint == 0x20ac and nextByte == 2)Arguments
| Name | Type | Description |
|---|---|---|
value | string | ByteView | the bytes to read |
byteOffset | integer | the 1-based byte to look back from |
Returns
| Type | Description |
|---|---|
integer? | the codepoint, or nil at the start of the value |
integer | the byte that codepoint starts at |
Raises
when byteOffset is not an integer, or is outside the value
utf8.encodefunction#
function utf8.encode(codepoint: integer): stringEncodes one codepoint as UTF-8 bytes.
Surrogate halves encode rather than raise, the way a lone half decodes to the replacement rather than stopping a walk: what refuses them is nupp.text.utf8.isValid, on the bytes, and encoding one is how a caller reproducing damaged text writes it back out.
Arguments
| Name | Type | Description |
|---|---|---|
codepoint | integer | the codepoint to encode |
Returns
| Type | Description |
|---|---|
string | its encoding |
Raises
when the codepoint is not an integer, or is outside Unicode
utf8.isValidfunction#
Reports whether the whole value is well-formed UTF-8.
Arguments
| Name | Type | Description |
|---|---|---|
value | string | ByteView | the bytes to test |
Returns
| Type | Description |
|---|---|
boolean | whether every sequence is well formed |
utf8.lengthfunction#
Counts the codepoints in the value.
A malformed byte counts as one.
Arguments
| Name | Type | Description |
|---|---|---|
value | string | ByteView | the bytes to measure |
Returns
| Type | Description |
|---|---|
integer | the codepoint count |
utf8.truncatefunction#
function utf8.truncate(text: string, maxBytes: integer): stringCuts text to at most maxBytes, never through a codepoint.
Arguments
| Name | Type | Description |
|---|---|---|
text | string | the text to cut |
maxBytes | integer | the most bytes the answer may use |
Returns
| Type | Description |
|---|---|
string | the cut text |
utf8.validPrefixLengthfunction#
function utf8.validPrefixLength(value: string | ByteView, maxBytes: integer): integerMeasures the longest well-formed prefix no longer than maxBytes.
const utf8 = nupp.text.utf8
-- "A€B" is 5 bytes; the 4-byte budget stops just short of "B" rather
-- than splitting "€" in half.
assert(utf8.validPrefixLength("A€B", 4) == 4)Arguments
| Name | Type | Description |
|---|---|---|
value | string | ByteView | the bytes to measure |
maxBytes | integer | the most bytes the prefix may use |
Returns
| Type | Description |
|---|---|
integer | the prefix length, in bytes |