# tecs.data.utf8 UTF-8 codepoint decoding, encoding, validation, and byte-safe truncation. Offsets are one-based byte cursor positions, matching Lua string indices. `decodeAt` reads the codepoint beginning at an offset and returns the cursor after it. `decodeBefore` reads the codepoint before a cursor and returns its starting offset: ```teal local utf8 = tecs.data.utf8 local text = "A€" local codepoint, nextOffset = utf8.decodeAt(text, 2) assert(codepoint == 0x20ac) assert(nextOffset == #text + 1) codepoint, nextOffset = utf8.decodeBefore(text, nextOffset) assert(codepoint == 0x20ac) assert(nextOffset == 2) ``` The end cursor is the byte length plus one. `decodeAt` returns nil there, and `decodeBefore` returns nil at cursor 1. An offset inside a multibyte sequence is not moved to a boundary automatically. SDL reports the malformed fragment at that exact position as U+FFFD. Every inspection function accepts a Lua string or an open [`ByteView`](/modules/io/#tecs.io.ByteView). A view is read through its retained pointer without first becoming a Lua string. `validPrefixLength` answers the byte count a caller can use to retain a zero-copy view at a UTF-8 boundary. Malformed UTF-8 is recoverable rather than exceptional. Forward decoding returns U+FFFD and consumes one malformed byte, so callers can keep scanning. A valid encoded U+FFFD remains indistinguishable by value but consumes its three bytes. Embedded NUL is the valid U+0000 codepoint and does not terminate a Lua string. These functions operate on Unicode codepoints, not user-perceived characters. A combining mark, variation selector, or member of an emoji sequence is a separate result. Text editing that needs cursor movement by grapheme cluster requires a Unicode segmentation implementation above this module. ## Module contents ### Functions | Function | Kind | Description | | --- | --- | --- | | [`decodeAt`](/modules/data/utf8/#tecs.data.utf8.decodeAt) | Static | Decodes the codepoint beginning at a byte offset. | | [`decodeBefore`](/modules/data/utf8/#tecs.data.utf8.decodeBefore) | Static | Decodes the codepoint before a byte cursor. | | [`encode`](/modules/data/utf8/#tecs.data.utf8.encode) | Static | Encodes one Unicode scalar value as UTF-8. | | [`isValid`](/modules/data/utf8/#tecs.data.utf8.isValid) | Static | Reports whether every byte forms canonical UTF-8. | | [`length`](/modules/data/utf8/#tecs.data.utf8.length) | Static | Counts Unicode codepoints in bytes. | | [`truncate`](/modules/data/utf8/#tecs.data.utf8.truncate) | Static | Truncates a string without splitting a valid UTF-8 sequence. | | [`validPrefixLength`](/modules/data/utf8/#tecs.data.utf8.validPrefixLength) | Static | Returns the longest prefix ending at a complete UTF-8 decoding unit. | ## Functions ### tecs.data.utf8.decodeAt Static Decodes the codepoint beginning at a byte offset. The function returns U+FFFD and advances one byte when the offset begins malformed or truncated UTF-8. It does not search for a nearby codepoint boundary when the caller supplies an offset inside a multibyte sequence. ```teal function tecs.data.utf8.decodeAt( bytes: ByteInput, byteOffset: integer ): integer | nil, integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). Embedded NUL is U+0000. | | `byteOffset` | `integer` | A one-based byte cursor from 1 through the byte length plus one. The final value is the end cursor. Other values raise. | #### Returns | Type | Description | | --- | --- | | integer | nil | The Unicode codepoint, or nil at the end cursor. | | `integer` | The one-based cursor immediately after the result, or the unchanged end cursor when no codepoint remains. | #### Examples Reads forward from a one-based byte offset. ```teal local utf8 = require("tecs.data.utf8") local codepoint, nextOffset = utf8.decodeAt("A€", 2) assert(codepoint == 0x20ac) assert(nextOffset == 5) ``` ### tecs.data.utf8.decodeBefore Static Decodes the codepoint before a byte cursor. The decoder searches backward for a possible sequence leader. When that candidate does not form one scalar ending at the cursor, it reports the final byte as U+FFFD instead. Forward and reverse scans therefore recover from malformed input one byte at a time. ```teal function tecs.data.utf8.decodeBefore( bytes: ByteInput, byteOffset: integer ): integer | nil, integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). Embedded NUL is U+0000. | | `byteOffset` | `integer` | A one-based byte cursor from 1 through the byte length plus one. It points after the codepoint to read. Other values raise. | #### Returns | Type | Description | | --- | --- | | integer | nil | The preceding Unicode codepoint, or nil at cursor 1. | | `integer` | The one-based starting offset of the result, or 1 when no codepoint precedes the cursor. | #### Examples Reads backward from the end cursor. ```teal local utf8 = require("tecs.data.utf8") local codepoint, startOffset = utf8.decodeBefore("A€", 5) assert(codepoint == 0x20ac) assert(startOffset == 2) ``` ### tecs.data.utf8.encode Static Encodes one Unicode scalar value as UTF-8. ```teal function tecs.data.utf8.encode(codepoint: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `codepoint` | `integer` | An integer from U+0000 through U+10FFFF, excluding the UTF-16 surrogate range U+D800 through U+DFFF. Other values raise. | #### Returns | Type | Description | | --- | --- | | `string` | One through four bytes. U+0000 returns a one-byte Lua string containing NUL. | #### Examples Encodes a Unicode scalar value. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.encode(0x20ac) == "€") ``` ### tecs.data.utf8.isValid Static Reports whether every byte forms canonical UTF-8. RFC 3629 rules reject overlong encodings, UTF-16 surrogate values, codepoints above U+10FFFF, stray continuation bytes, and truncated sequences. Embedded NUL and an encoded U+FFFD are valid. ```teal function tecs.data.utf8.isValid(bytes: ByteInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView), including embedded NUL. | #### Returns | Type | Description | | --- | --- | | `boolean` | True only when forward decoding never needs a replacement for a malformed byte. | #### Examples Rejects malformed UTF-8 bytes. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.isValid("café")) assert(not utf8.isValid("\xff")) ``` ### tecs.data.utf8.length Static Counts Unicode codepoints in bytes. This is not a byte count or a grapheme count. Each malformed byte contributes one U+FFFD result, while an embedded NUL contributes the valid U+0000 codepoint and does not end the input. ```teal function tecs.data.utf8.length(bytes: ByteInput): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView), including embedded NUL. | #### Returns | Type | Description | | --- | --- | | `integer` | The number of forward decoding steps. | #### Examples Counts Unicode codepoints rather than bytes. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.length("A€") == 2) ``` ### tecs.data.utf8.truncate Static Truncates a string without splitting a valid UTF-8 sequence. The result is a byte prefix, not a normalization. Malformed bytes are one-byte decoding units and remain unchanged when they fit. The function does not combine marks or emoji into grapheme clusters. ```teal function tecs.data.utf8.truncate( text: string, maxBytes: integer ): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | The complete byte string. | | `maxBytes` | `integer` | A non-negative integer byte ceiling. Other values raise. | #### Returns | Type | Description | | --- | --- | | `string` | The longest prefix no larger than `maxBytes` whose final decoding unit is complete. Returns `text` itself when it fits. | #### Examples Leaves a multibyte codepoint intact at the byte limit. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.truncate("A€B", 4) == "A€") ``` ### tecs.data.utf8.validPrefixLength Static Returns the longest prefix ending at a complete UTF-8 decoding unit. The function returns a byte count and never copies the input. A caller can pass the result to `ByteView:view` to retain the prefix. Malformed bytes remain one-byte units and therefore fit whenever their byte does. ```teal function tecs.data.utf8.validPrefixLength( bytes: ByteInput, maxBytes: integer ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). | | `maxBytes` | `integer` | A non-negative integer byte ceiling. Other values raise. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the largest safe prefix length no greater than `maxBytes`. | #### Examples Retains a zero-copy prefix at a complete codepoint boundary. ```teal local tecs = require("tecs") local bytes = tecs.io.newBuffer("A€B") local all = bytes:view() local count = tecs.data.utf8.validPrefixLength(all, 3) local prefix = all:view(0, count) assert(prefix:getString() == "A") prefix:close() all:close() bytes:close() ```