mirror of
https://github.com/Ed94/Odin.git
synced 2026-07-29 19:00:06 +00:00
Merge remote-tracking branch 'offical/master'
# Conflicts: # src/check_builtin.cpp
This commit is contained in:
@@ -87,7 +87,7 @@ init_cmp :: proc(
|
||||
init_ordered :: proc(
|
||||
t: ^$T/Tree($Value),
|
||||
node_allocator := context.allocator,
|
||||
) where intrinsics.type_is_ordered_numeric(Value) {
|
||||
) where intrinsics.type_is_ordered(Value) {
|
||||
init_cmp(t, slice.cmp_proc(Value), node_allocator)
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ init_cmp :: proc(t: ^$T/Tree($Key, $Value), cmp_fn: proc(a, b: Key) -> Ordering,
|
||||
|
||||
// init_ordered initializes a tree containing ordered keys, with
|
||||
// a comparison function that results in an ascending order sort.
|
||||
init_ordered :: proc(t: ^$T/Tree($Key, $Value), node_allocator := context.allocator) where intrinsics.type_is_ordered_numeric(Key) {
|
||||
init_ordered :: proc(t: ^$T/Tree($Key, $Value), node_allocator := context.allocator) where intrinsics.type_is_ordered(Key) {
|
||||
init_cmp(t, slice.cmp_proc(Key), node_allocator)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Feoramund
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,38 @@
|
||||
package flags
|
||||
|
||||
import "core:time"
|
||||
|
||||
// Set to true to compile with support for core named types disabled, as a
|
||||
// fallback in the event your platform does not support one of the types, or
|
||||
// you have no need for them and want a smaller binary.
|
||||
NO_CORE_NAMED_TYPES :: #config(ODIN_CORE_FLAGS_NO_CORE_NAMED_TYPES, false)
|
||||
|
||||
// Override support for parsing `time` types.
|
||||
IMPORTING_TIME :: #config(ODIN_CORE_FLAGS_USE_TIME, time.IS_SUPPORTED)
|
||||
|
||||
// Override support for parsing `net` types.
|
||||
// TODO: Update this when the BSDs are supported.
|
||||
IMPORTING_NET :: #config(ODIN_CORE_FLAGS_USE_NET, ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .Darwin)
|
||||
|
||||
TAG_ARGS :: "args"
|
||||
SUBTAG_NAME :: "name"
|
||||
SUBTAG_POS :: "pos"
|
||||
SUBTAG_REQUIRED :: "required"
|
||||
SUBTAG_HIDDEN :: "hidden"
|
||||
SUBTAG_VARIADIC :: "variadic"
|
||||
SUBTAG_FILE :: "file"
|
||||
SUBTAG_PERMS :: "perms"
|
||||
SUBTAG_INDISTINCT :: "indistinct"
|
||||
|
||||
TAG_USAGE :: "usage"
|
||||
|
||||
UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>"
|
||||
|
||||
INTERNAL_VARIADIC_FLAG :: "varg"
|
||||
|
||||
RESERVED_HELP_FLAG :: "help"
|
||||
RESERVED_HELP_FLAG_SHORT :: "h"
|
||||
|
||||
// If there are more than this number of flags in total, only the required and
|
||||
// positional flags will be shown in the one-line usage summary.
|
||||
ONE_LINE_FLAG_CUTOFF_COUNT :: 16
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
package flags implements a command-line argument parser.
|
||||
|
||||
It works by using Odin's run-time type information to determine where and how
|
||||
to store data on a struct provided by the program. Type conversion is handled
|
||||
automatically and errors are reported with useful messages.
|
||||
|
||||
|
||||
Command-Line Syntax:
|
||||
|
||||
Arguments are treated differently depending on how they're formatted.
|
||||
The format is similar to the Odin binary's way of handling compiler flags.
|
||||
|
||||
```
|
||||
type handling
|
||||
------------ ------------------------
|
||||
<positional> depends on struct layout
|
||||
-<flag> set a bool true
|
||||
-<flag:option> set flag to option
|
||||
-<flag=option> set flag to option, alternative syntax
|
||||
-<map>:<key>=<value> set map[key] to value
|
||||
```
|
||||
|
||||
|
||||
Struct Tags:
|
||||
|
||||
Users of the `core:encoding/json` package may be familiar with using tags to
|
||||
annotate struct metadata. The same technique is used here to annotate where
|
||||
arguments should go and which are required.
|
||||
|
||||
Under the `args` tag, there are the following subtags:
|
||||
|
||||
- `name=S`: set `S` as the flag's name.
|
||||
- `pos=N`: place positional argument `N` into this flag.
|
||||
- `hidden`: hide this flag from the usage documentation.
|
||||
- `required`: cause verification to fail if this argument is not set.
|
||||
- `variadic`: take all remaining arguments when set, UNIX-style only.
|
||||
- `file`: for `os.Handle` types, file open mode.
|
||||
- `perms`: for `os.Handle` types, file open permissions.
|
||||
- `indistinct`: allow the setting of distinct types by their base type.
|
||||
|
||||
`required` may be given a range specifier in the following formats:
|
||||
```
|
||||
min
|
||||
<max
|
||||
min<max
|
||||
```
|
||||
|
||||
`max` is not inclusive in this range, as noted by the less-than `<` sign, so if
|
||||
you want to require 3 and only 3 arguments in a dynamic array, you would
|
||||
specify `required=3<4`.
|
||||
|
||||
|
||||
`variadic` may be given a number (`variadic=N`) above 1 to limit how many extra
|
||||
arguments it consumes.
|
||||
|
||||
|
||||
`file` determines the file open mode for an `os.Handle`.
|
||||
It accepts a string of flags that can be mixed together:
|
||||
- r: read
|
||||
- w: write
|
||||
- c: create, create the file if it doesn't exist
|
||||
- a: append, add any new writes to the end of the file
|
||||
- t: truncate, erase the file on open
|
||||
|
||||
|
||||
`perms` determines the file open permissions for an `os.Handle`.
|
||||
|
||||
The permissions are represented by three numbers in octal format. The first
|
||||
number is the owner, the second is the group, and the third is other. Read is
|
||||
represented by 4, write by 2, and execute by 1.
|
||||
|
||||
These numbers are added together to get combined permissions. For example, 644
|
||||
represents read/write for the owner, read for the group, and read for other.
|
||||
|
||||
Note that this may only have effect on UNIX-like platforms. By default, `perms`
|
||||
is set to 444 when only reading and 644 when writing.
|
||||
|
||||
|
||||
`indistinct` tells the parser that it's okay to treat distinct types as their
|
||||
underlying base type. Normally, the parser will hand those types off to the
|
||||
custom type setter (more about that later) if one is available, if it doesn't
|
||||
know how to handle the type.
|
||||
|
||||
|
||||
Usage Tag:
|
||||
|
||||
There is also the `usage` tag, which is a plain string to be printed alongside
|
||||
the flag in the usage output. If `usage` contains a newline, it will be
|
||||
properly aligned when printed.
|
||||
|
||||
All surrounding whitespace is trimmed when formatting with multiple lines.
|
||||
|
||||
|
||||
Supported Flag Data Types:
|
||||
|
||||
- all booleans
|
||||
- all integers
|
||||
- all floats
|
||||
- all enums
|
||||
- all complex numbers
|
||||
- all quaternions
|
||||
- all bit_sets
|
||||
- `string` and `cstring`
|
||||
- `rune`
|
||||
- `os.Handle`
|
||||
- `time.Time`
|
||||
- `datetime.DateTime`
|
||||
- `net.Host_Or_Endpoint`,
|
||||
- additional custom types, see Custom Types below
|
||||
- `dynamic` arrays with element types of the above
|
||||
- `map[string]`s or `map[cstring]`s with value types of the above
|
||||
|
||||
|
||||
Validation:
|
||||
|
||||
The parser will ensure `required` arguments are set, if no errors occurred
|
||||
during parsing. This is on by default.
|
||||
|
||||
Additionally, you may call `register_flag_checker` to set your own argument
|
||||
validation procedure that will be called after the default checker.
|
||||
|
||||
|
||||
Strict:
|
||||
|
||||
The parser will return on the first error and stop parsing. This is on by
|
||||
default. Otherwise, all arguments that can be parsed, will be, and only the
|
||||
last error is returned.
|
||||
|
||||
|
||||
Error Messages:
|
||||
|
||||
All error message strings are allocated using the context's `temp_allocator`,
|
||||
so if you need them to persist, make sure to clone the underlying `message`.
|
||||
|
||||
|
||||
Help:
|
||||
|
||||
By default, `-h` and `-help` are reserved flags which raise their own error
|
||||
type when set, allowing the program to handle the request differently from
|
||||
other errors.
|
||||
|
||||
|
||||
Custom Types:
|
||||
|
||||
You may specify your own type setter for program-specific structs and other
|
||||
named types. Call `register_type_setter` with an appropriate proc before
|
||||
calling any of the parsing procs.
|
||||
|
||||
A compliant `Custom_Type_Setter` must return three values:
|
||||
- an error message if one occurred,
|
||||
- a boolean indicating if the proc handles the type, and
|
||||
- an `Allocator_Error` if any occurred.
|
||||
|
||||
If the setter does not handle the type, simply return without setting any of
|
||||
the values.
|
||||
|
||||
|
||||
UNIX-style:
|
||||
|
||||
This package also supports parsing arguments in a limited flavor of UNIX.
|
||||
Odin and UNIX style are mutually exclusive, and which one to be used is chosen
|
||||
at parse time.
|
||||
|
||||
```
|
||||
--flag
|
||||
--flag=argument
|
||||
--flag argument
|
||||
--flag argument repeating-argument
|
||||
```
|
||||
|
||||
`-flag` may also be substituted for `--flag`.
|
||||
|
||||
Do note that map flags are not currently supported in this parsing style.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
A complete example is given in the `example` subdirectory.
|
||||
*/
|
||||
package flags
|
||||
@@ -0,0 +1,50 @@
|
||||
package flags
|
||||
|
||||
import "core:os"
|
||||
|
||||
Parse_Error_Reason :: enum {
|
||||
None,
|
||||
// An extra positional argument was given, and there is no `varg` field.
|
||||
Extra_Positional,
|
||||
// The underlying type does not support the string value it is being set to.
|
||||
Bad_Value,
|
||||
// No flag was given by the user.
|
||||
No_Flag,
|
||||
// No value was given by the user.
|
||||
No_Value,
|
||||
// The flag on the struct is missing.
|
||||
Missing_Flag,
|
||||
// The type itself isn't supported.
|
||||
Unsupported_Type,
|
||||
}
|
||||
|
||||
// Raised during parsing, naturally.
|
||||
Parse_Error :: struct {
|
||||
reason: Unified_Parse_Error_Reason,
|
||||
message: string,
|
||||
}
|
||||
|
||||
// Raised during parsing.
|
||||
// Provides more granular information than what just a string could hold.
|
||||
Open_File_Error :: struct {
|
||||
filename: string,
|
||||
errno: os.Errno,
|
||||
mode: int,
|
||||
perms: int,
|
||||
}
|
||||
|
||||
// Raised during parsing.
|
||||
Help_Request :: distinct bool
|
||||
|
||||
|
||||
// Raised after parsing, during validation.
|
||||
Validation_Error :: struct {
|
||||
message: string,
|
||||
}
|
||||
|
||||
Error :: union {
|
||||
Parse_Error,
|
||||
Open_File_Error,
|
||||
Help_Request,
|
||||
Validation_Error,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//+build freebsd, netbsd, openbsd
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
Unified_Parse_Error_Reason :: union #shared_nil {
|
||||
Parse_Error_Reason,
|
||||
runtime.Allocator_Error,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build !freebsd !netbsd !openbsd
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
import "core:net"
|
||||
|
||||
Unified_Parse_Error_Reason :: union #shared_nil {
|
||||
Parse_Error_Reason,
|
||||
runtime.Allocator_Error,
|
||||
net.Parse_Endpoint_Error,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package core_flags_example
|
||||
|
||||
import "base:runtime"
|
||||
import "core:flags"
|
||||
import "core:fmt"
|
||||
import "core:net"
|
||||
import "core:os"
|
||||
import "core:time/datetime"
|
||||
|
||||
|
||||
Fixed_Point1_1 :: struct {
|
||||
integer: u8,
|
||||
fractional: u8,
|
||||
}
|
||||
|
||||
Optimization_Level :: enum {
|
||||
Slow,
|
||||
Fast,
|
||||
Warp_Speed,
|
||||
Ludicrous_Speed,
|
||||
}
|
||||
|
||||
// It's simple but powerful.
|
||||
my_custom_type_setter :: proc(
|
||||
data: rawptr,
|
||||
data_type: typeid,
|
||||
unparsed_value: string,
|
||||
args_tag: string,
|
||||
) -> (
|
||||
error: string,
|
||||
handled: bool,
|
||||
alloc_error: runtime.Allocator_Error,
|
||||
) {
|
||||
if data_type == Fixed_Point1_1 {
|
||||
handled = true
|
||||
ptr := cast(^Fixed_Point1_1)data
|
||||
|
||||
// precision := flags.get_subtag(args_tag, "precision")
|
||||
|
||||
if len(unparsed_value) == 3 {
|
||||
ptr.integer = unparsed_value[0] - '0'
|
||||
ptr.fractional = unparsed_value[2] - '0'
|
||||
} else {
|
||||
error = "Incorrect format. Must be in the form of `i.f`."
|
||||
}
|
||||
|
||||
// Perform sanity checking here in the type parsing phase.
|
||||
//
|
||||
// The validation phase is flag-specific.
|
||||
if !(0 <= ptr.integer && ptr.integer < 10) || !(0 <= ptr.fractional && ptr.fractional < 10) {
|
||||
error = "Incorrect format. Must be between `0.0` and `9.9`."
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
my_custom_flag_checker :: proc(
|
||||
model: rawptr,
|
||||
name: string,
|
||||
value: any,
|
||||
args_tag: string,
|
||||
) -> (error: string) {
|
||||
if name == "iterations" {
|
||||
v := value.(int)
|
||||
if !(1 <= v && v < 5) {
|
||||
error = "Iterations only supports 1 ..< 5."
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Distinct_Int :: distinct int
|
||||
|
||||
main :: proc() {
|
||||
Options :: struct {
|
||||
|
||||
file: os.Handle `args:"pos=0,required,file=r" usage:"Input file."`,
|
||||
output: os.Handle `args:"pos=1,file=cw" usage:"Output file."`,
|
||||
|
||||
hub: net.Host_Or_Endpoint `usage:"Internet address to contact for updates."`,
|
||||
schedule: datetime.DateTime `usage:"Launch tasks at this time."`,
|
||||
|
||||
opt: Optimization_Level `usage:"Optimization level."`,
|
||||
todo: [dynamic]string `usage:"Todo items."`,
|
||||
|
||||
accuracy: Fixed_Point1_1 `args:"required" usage:"Lenience in FLOP calculations."`,
|
||||
iterations: int `usage:"Run this many times."`,
|
||||
|
||||
// Note how the parser will transform this flag's name into `special-int`.
|
||||
special_int: Distinct_Int `args:"indistinct" usage:"Able to set distinct types."`,
|
||||
|
||||
quat: quaternion256,
|
||||
|
||||
bits: bit_set[0..<8],
|
||||
|
||||
// Many different requirement styles:
|
||||
|
||||
// gadgets: [dynamic]string `args:"required=1" usage:"gadgets"`,
|
||||
// widgets: [dynamic]string `args:"required=<3" usage:"widgets"`,
|
||||
// foos: [dynamic]string `args:"required=2<4"`,
|
||||
// bars: [dynamic]string `args:"required=3<4"`,
|
||||
// bots: [dynamic]string `args:"required"`,
|
||||
|
||||
// (Maps) Only available in Odin style:
|
||||
|
||||
// assignments: map[string]u8 `args:"name=assign" usage:"Number of jobs per worker."`,
|
||||
|
||||
// (Variadic) Only available in UNIX style:
|
||||
|
||||
// bots: [dynamic]string `args:"variadic=2,required"`,
|
||||
|
||||
verbose: bool `usage:"Show verbose output."`,
|
||||
debug: bool `args:"hidden" usage:"print debug info"`,
|
||||
|
||||
varg: [dynamic]string `usage:"Any extra arguments go here."`,
|
||||
}
|
||||
|
||||
opt: Options
|
||||
style : flags.Parsing_Style = .Odin
|
||||
|
||||
flags.register_type_setter(my_custom_type_setter)
|
||||
flags.register_flag_checker(my_custom_flag_checker)
|
||||
flags.parse_or_exit(&opt, os.args, style)
|
||||
|
||||
fmt.printfln("%#v", opt)
|
||||
|
||||
if opt.output != 0 {
|
||||
os.write_string(opt.output, "Hellope!\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//+private
|
||||
package flags
|
||||
|
||||
import "base:intrinsics"
|
||||
@require import "base:runtime"
|
||||
import "core:container/bit_array"
|
||||
@require import "core:fmt"
|
||||
@require import "core:mem"
|
||||
import "core:reflect"
|
||||
@require import "core:strconv"
|
||||
@require import "core:strings"
|
||||
|
||||
// Push a positional argument onto a data struct, checking for specified
|
||||
// positionals first before adding it to a fallback field.
|
||||
@(optimization_mode="size")
|
||||
push_positional :: #force_no_inline proc (model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
|
||||
if bit_array.get(&parser.filled_pos, parser.filled_pos.max_index) {
|
||||
// The max index is set, which means we're out of space.
|
||||
// Add one free bit by setting the index above to false.
|
||||
bit_array.set(&parser.filled_pos, 1 + parser.filled_pos.max_index, false)
|
||||
}
|
||||
|
||||
pos: int = ---
|
||||
{
|
||||
iter := bit_array.make_iterator(&parser.filled_pos)
|
||||
ok: bool
|
||||
pos, ok = bit_array.iterate_by_unset(&iter)
|
||||
|
||||
// This may be an allocator error.
|
||||
assert(ok, "Unable to find a free spot in the positional bit_array.")
|
||||
}
|
||||
|
||||
field, index, has_pos_assigned := get_field_by_pos(model, pos)
|
||||
|
||||
if !has_pos_assigned {
|
||||
when intrinsics.type_has_field(T, INTERNAL_VARIADIC_FLAG) {
|
||||
// Add it to the fallback array.
|
||||
field = reflect.struct_field_by_name(T, INTERNAL_VARIADIC_FLAG)
|
||||
} else {
|
||||
return Parse_Error {
|
||||
.Extra_Positional,
|
||||
fmt.tprintf("Got extra positional argument `%s` with nowhere to store it.", arg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
|
||||
args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
|
||||
field_name := get_field_name(field)
|
||||
error = parse_and_set_pointer_by_type(ptr, arg, field.type, args_tag)
|
||||
#partial switch &specific_error in error {
|
||||
case Parse_Error:
|
||||
specific_error.message = fmt.tprintf("Unable to set positional #%i (%s) of type %v to `%s`.%s%s",
|
||||
pos,
|
||||
field_name,
|
||||
field.type,
|
||||
arg,
|
||||
" " if len(specific_error.message) > 0 else "",
|
||||
specific_error.message)
|
||||
case nil:
|
||||
bit_array.set(&parser.filled_pos, pos)
|
||||
bit_array.set(&parser.fields_set, index)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
register_field :: proc(parser: ^Parser, field: reflect.Struct_Field, index: int) {
|
||||
if pos, ok := get_field_pos(field); ok {
|
||||
bit_array.set(&parser.filled_pos, pos)
|
||||
}
|
||||
|
||||
bit_array.set(&parser.fields_set, index)
|
||||
}
|
||||
|
||||
// Set a `-flag` argument, Odin-style.
|
||||
@(optimization_mode="size")
|
||||
set_odin_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (error: Error) {
|
||||
// We make a special case for help requests.
|
||||
switch name {
|
||||
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
|
||||
return Help_Request{}
|
||||
}
|
||||
|
||||
field, index := get_field_by_name(model, name) or_return
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Boolean:
|
||||
ptr := cast(^bool)(cast(uintptr)model + field.offset)
|
||||
ptr^ = true
|
||||
case:
|
||||
return Parse_Error {
|
||||
.Bad_Value,
|
||||
fmt.tprintf("Unable to set `%s` of type %v to true.", name, field.type),
|
||||
}
|
||||
}
|
||||
|
||||
register_field(parser, field, index)
|
||||
return
|
||||
}
|
||||
|
||||
// Set a `-flag` argument, UNIX-style.
|
||||
@(optimization_mode="size")
|
||||
set_unix_flag :: proc(model: ^$T, parser: ^Parser, name: string) -> (future_args: int, error: Error) {
|
||||
// We make a special case for help requests.
|
||||
switch name {
|
||||
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
|
||||
return 0, Help_Request{}
|
||||
}
|
||||
|
||||
field, index := get_field_by_name(model, name) or_return
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Boolean:
|
||||
ptr := cast(^bool)(cast(uintptr)model + field.offset)
|
||||
ptr^ = true
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
future_args = 1
|
||||
if tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
||||
if length, is_variadic := get_struct_subtag(tag, SUBTAG_VARIADIC); is_variadic {
|
||||
// Variadic arrays may specify how many arguments they consume at once.
|
||||
// Otherwise, they take everything that's left.
|
||||
if value, value_ok := strconv.parse_u64_of_base(length, 10); value_ok {
|
||||
future_args = cast(int)value
|
||||
} else {
|
||||
future_args = max(int)
|
||||
}
|
||||
}
|
||||
}
|
||||
case:
|
||||
// `--flag`, waiting on its value.
|
||||
future_args = 1
|
||||
}
|
||||
|
||||
register_field(parser, field, index)
|
||||
return
|
||||
}
|
||||
|
||||
// Set a `-flag:option` argument.
|
||||
@(optimization_mode="size")
|
||||
set_option :: proc(model: ^$T, parser: ^Parser, name, option: string) -> (error: Error) {
|
||||
field, index := get_field_by_name(model, name) or_return
|
||||
|
||||
if len(option) == 0 {
|
||||
return Parse_Error {
|
||||
.No_Value,
|
||||
fmt.tprintf("Setting `%s` to an empty value is meaningless.", name),
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against incorrect syntax.
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
return Parse_Error {
|
||||
.No_Value,
|
||||
fmt.tprintf("Unable to set `%s` of type %v to `%s`. Are you missing an `=`? The correct format is `map:key=value`.", name, field.type, option),
|
||||
}
|
||||
}
|
||||
|
||||
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
|
||||
args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
|
||||
error = parse_and_set_pointer_by_type(ptr, option, field.type, args_tag)
|
||||
#partial switch &specific_error in error {
|
||||
case Parse_Error:
|
||||
specific_error.message = fmt.tprintf("Unable to set `%s` of type %v to `%s`.%s%s",
|
||||
name,
|
||||
field.type,
|
||||
option,
|
||||
" " if len(specific_error.message) > 0 else "",
|
||||
specific_error.message)
|
||||
case nil:
|
||||
register_field(parser, field, index)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Set a `-map:key=value` argument.
|
||||
@(optimization_mode="size")
|
||||
set_key_value :: proc(model: ^$T, parser: ^Parser, name, key, value: string) -> (error: Error) {
|
||||
field, index := get_field_by_name(model, name) or_return
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
key := key
|
||||
key_ptr := cast(rawptr)&key
|
||||
key_cstr: cstring
|
||||
if reflect.is_cstring(specific_type_info.key) {
|
||||
// We clone the key here, because it's liable to be a slice of an
|
||||
// Odin string, and we need to put a NUL terminator in it.
|
||||
key_cstr = strings.clone_to_cstring(key)
|
||||
key_ptr = &key_cstr
|
||||
}
|
||||
defer if key_cstr != nil {
|
||||
delete(key_cstr)
|
||||
}
|
||||
|
||||
raw_map := (^runtime.Raw_Map)(cast(uintptr)model + field.offset)
|
||||
|
||||
hash := specific_type_info.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^))
|
||||
|
||||
backing_alloc := false
|
||||
elem_backing: []byte
|
||||
value_ptr: rawptr
|
||||
|
||||
if raw_map.allocator.procedure == nil {
|
||||
raw_map.allocator = context.allocator
|
||||
} else {
|
||||
value_ptr = runtime.__dynamic_map_get(raw_map,
|
||||
specific_type_info.map_info,
|
||||
hash,
|
||||
key_ptr,
|
||||
)
|
||||
}
|
||||
|
||||
if value_ptr == nil {
|
||||
alloc_error: runtime.Allocator_Error = ---
|
||||
elem_backing, alloc_error = mem.alloc_bytes(specific_type_info.value.size, specific_type_info.value.align)
|
||||
if elem_backing == nil {
|
||||
return Parse_Error {
|
||||
alloc_error,
|
||||
"Failed to allocate element backing for map value.",
|
||||
}
|
||||
}
|
||||
|
||||
backing_alloc = true
|
||||
value_ptr = raw_data(elem_backing)
|
||||
}
|
||||
|
||||
args_tag, _ := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
|
||||
error = parse_and_set_pointer_by_type(value_ptr, value, specific_type_info.value, args_tag)
|
||||
#partial switch &specific_error in error {
|
||||
case Parse_Error:
|
||||
specific_error.message = fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.%s%s",
|
||||
name,
|
||||
field.type,
|
||||
key,
|
||||
value,
|
||||
" " if len(specific_error.message) > 0 else "",
|
||||
specific_error.message)
|
||||
}
|
||||
|
||||
if backing_alloc {
|
||||
runtime.__dynamic_map_set(raw_map,
|
||||
specific_type_info.map_info,
|
||||
hash,
|
||||
key_ptr,
|
||||
value_ptr,
|
||||
)
|
||||
|
||||
delete(elem_backing)
|
||||
}
|
||||
|
||||
register_field(parser, field, index)
|
||||
return
|
||||
}
|
||||
|
||||
return Parse_Error {
|
||||
.Bad_Value,
|
||||
fmt.tprintf("Unable to set `%s` of type %v with key=value: `%s`=`%s`.", name, field.type, key, value),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//+private
|
||||
package flags
|
||||
|
||||
import "core:container/bit_array"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
|
||||
// Used to group state together.
|
||||
Parser :: struct {
|
||||
// `fields_set` tracks which arguments have been set.
|
||||
// It uses their struct field index.
|
||||
fields_set: bit_array.Bit_Array,
|
||||
|
||||
// `filled_pos` tracks which arguments have been filled into positional
|
||||
// spots, much like how `fmt` treats them.
|
||||
filled_pos: bit_array.Bit_Array,
|
||||
}
|
||||
|
||||
parse_one_odin_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (error: Error) {
|
||||
arg := arg
|
||||
|
||||
if strings.has_prefix(arg, "-") {
|
||||
arg = arg[1:]
|
||||
|
||||
flag: string
|
||||
assignment_rune: rune
|
||||
find_assignment: for r, i in arg {
|
||||
switch r {
|
||||
case ':', '=':
|
||||
assignment_rune = r
|
||||
flag = arg[:i]
|
||||
arg = arg[1 + i:]
|
||||
break find_assignment
|
||||
case:
|
||||
continue find_assignment
|
||||
}
|
||||
}
|
||||
|
||||
if assignment_rune == 0 {
|
||||
if len(arg) == 0 {
|
||||
return Parse_Error {
|
||||
.No_Flag,
|
||||
"No flag was given.",
|
||||
}
|
||||
}
|
||||
|
||||
// -flag
|
||||
set_odin_flag(model, parser, arg) or_return
|
||||
|
||||
} else if assignment_rune == ':' {
|
||||
// -flag:option <OR> -map:key=value
|
||||
error = set_option(model, parser, flag, arg)
|
||||
|
||||
if error != nil {
|
||||
// -flag:option did not work, so this may be a -map:key=value set.
|
||||
find_equals: for r, i in arg {
|
||||
if r == '=' {
|
||||
key := arg[:i]
|
||||
arg = arg[1 + i:]
|
||||
error = set_key_value(model, parser, flag, key, arg)
|
||||
break find_equals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// -flag=option, alternative syntax
|
||||
set_option(model, parser, flag, arg) or_return
|
||||
}
|
||||
|
||||
} else {
|
||||
// positional
|
||||
error = push_positional(model, parser, arg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parse_one_unix_arg :: proc(model: ^$T, parser: ^Parser, arg: string) -> (
|
||||
future_args: int,
|
||||
current_flag: string,
|
||||
error: Error,
|
||||
) {
|
||||
arg := arg
|
||||
|
||||
if strings.has_prefix(arg, "-") {
|
||||
// -flag
|
||||
arg = arg[1:]
|
||||
|
||||
if strings.has_prefix(arg, "-") {
|
||||
// Allow `--` to function as `-`.
|
||||
arg = arg[1:]
|
||||
|
||||
if len(arg) == 0 {
|
||||
// `--`, and only `--`.
|
||||
// Everything from now on will be treated as an argument.
|
||||
future_args = max(int)
|
||||
current_flag = INTERNAL_VARIADIC_FLAG
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
flag: string
|
||||
find_assignment: for r, i in arg {
|
||||
if r == '=' {
|
||||
// --flag=option
|
||||
flag = arg[:i]
|
||||
arg = arg[1 + i:]
|
||||
error = set_option(model, parser, flag, arg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --flag option, potentially
|
||||
future_args = set_unix_flag(model, parser, arg) or_return
|
||||
current_flag = arg
|
||||
|
||||
} else {
|
||||
// positional
|
||||
error = push_positional(model, parser, arg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Parse a number of requirements specifier.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// `min`
|
||||
// `<max`
|
||||
// `min<max`
|
||||
parse_requirements :: proc(str: string) -> (minimum, maximum: int, ok: bool) {
|
||||
if len(str) == 0 {
|
||||
return 1, max(int), true
|
||||
}
|
||||
|
||||
if less_than := strings.index_byte(str, '<'); less_than != -1 {
|
||||
if len(str) == 1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
#no_bounds_check left := str[:less_than]
|
||||
#no_bounds_check right := str[1 + less_than:]
|
||||
|
||||
if left_value, parse_ok := strconv.parse_u64_of_base(left, 10); parse_ok {
|
||||
minimum = cast(int)left_value
|
||||
} else if len(left) > 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
if right_value, parse_ok := strconv.parse_u64_of_base(right, 10); parse_ok {
|
||||
maximum = cast(int)right_value
|
||||
} else if len(right) > 0 {
|
||||
return 0, 0, false
|
||||
} else {
|
||||
maximum = max(int)
|
||||
}
|
||||
} else {
|
||||
if value, parse_ok := strconv.parse_u64_of_base(str, 10); parse_ok {
|
||||
minimum = cast(int)value
|
||||
maximum = max(int)
|
||||
} else {
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
//+private
|
||||
package flags
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:reflect"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
@require import "core:time"
|
||||
@require import "core:time/datetime"
|
||||
import "core:unicode/utf8"
|
||||
|
||||
@(optimization_mode="size")
|
||||
parse_and_set_pointer_by_base_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info) -> bool {
|
||||
bounded_int :: proc(value, min, max: i128) -> (result: i128, ok: bool) {
|
||||
return value, min <= value && value <= max
|
||||
}
|
||||
|
||||
bounded_uint :: proc(value, max: u128) -> (result: u128, ok: bool) {
|
||||
return value, value <= max
|
||||
}
|
||||
|
||||
// NOTE(Feoramund): This procedure has been written with the goal in mind
|
||||
// of generating the least amount of assembly, given that this library is
|
||||
// likely to be called once and forgotten.
|
||||
//
|
||||
// I've rewritten the switch tables below in 3 different ways, and the
|
||||
// current one generates the least amount of code for me on Linux AMD64.
|
||||
//
|
||||
// The other two ways were:
|
||||
//
|
||||
// - the original implementation: use of parametric polymorphism which led
|
||||
// to dozens of functions generated, one for each type.
|
||||
//
|
||||
// - a `value, ok` assignment statement with the `or_return` done at the
|
||||
// end of the switch, instead of inline.
|
||||
//
|
||||
// This seems to be the smallest way for now.
|
||||
|
||||
#partial switch specific_type_info in type_info.variant {
|
||||
case runtime.Type_Info_Integer:
|
||||
if specific_type_info.signed {
|
||||
value := strconv.parse_i128(str) or_return
|
||||
switch type_info.id {
|
||||
case i8: (cast(^i8) ptr)^ = cast(i8) bounded_int(value, cast(i128)min(i8), cast(i128)max(i8) ) or_return
|
||||
case i16: (cast(^i16) ptr)^ = cast(i16) bounded_int(value, cast(i128)min(i16), cast(i128)max(i16) ) or_return
|
||||
case i32: (cast(^i32) ptr)^ = cast(i32) bounded_int(value, cast(i128)min(i32), cast(i128)max(i32) ) or_return
|
||||
case i64: (cast(^i64) ptr)^ = cast(i64) bounded_int(value, cast(i128)min(i64), cast(i128)max(i64) ) or_return
|
||||
case i128: (cast(^i128) ptr)^ = value
|
||||
|
||||
case int: (cast(^int) ptr)^ = cast(int) bounded_int(value, cast(i128)min(int), cast(i128)max(int) ) or_return
|
||||
|
||||
case i16le: (cast(^i16le) ptr)^ = cast(i16le) bounded_int(value, cast(i128)min(i16le), cast(i128)max(i16le) ) or_return
|
||||
case i32le: (cast(^i32le) ptr)^ = cast(i32le) bounded_int(value, cast(i128)min(i32le), cast(i128)max(i32le) ) or_return
|
||||
case i64le: (cast(^i64le) ptr)^ = cast(i64le) bounded_int(value, cast(i128)min(i64le), cast(i128)max(i64le) ) or_return
|
||||
case i128le: (cast(^i128le)ptr)^ = cast(i128le) bounded_int(value, cast(i128)min(i128le), cast(i128)max(i128le)) or_return
|
||||
|
||||
case i16be: (cast(^i16be) ptr)^ = cast(i16be) bounded_int(value, cast(i128)min(i16be), cast(i128)max(i16be) ) or_return
|
||||
case i32be: (cast(^i32be) ptr)^ = cast(i32be) bounded_int(value, cast(i128)min(i32be), cast(i128)max(i32be) ) or_return
|
||||
case i64be: (cast(^i64be) ptr)^ = cast(i64be) bounded_int(value, cast(i128)min(i64be), cast(i128)max(i64be) ) or_return
|
||||
case i128be: (cast(^i128be)ptr)^ = cast(i128be) bounded_int(value, cast(i128)min(i128be), cast(i128)max(i128be)) or_return
|
||||
}
|
||||
} else {
|
||||
value := strconv.parse_u128(str) or_return
|
||||
switch type_info.id {
|
||||
case u8: (cast(^u8) ptr)^ = cast(u8) bounded_uint(value, cast(u128)max(u8) ) or_return
|
||||
case u16: (cast(^u16) ptr)^ = cast(u16) bounded_uint(value, cast(u128)max(u16) ) or_return
|
||||
case u32: (cast(^u32) ptr)^ = cast(u32) bounded_uint(value, cast(u128)max(u32) ) or_return
|
||||
case u64: (cast(^u64) ptr)^ = cast(u64) bounded_uint(value, cast(u128)max(u64) ) or_return
|
||||
case u128: (cast(^u128) ptr)^ = value
|
||||
|
||||
case uint: (cast(^uint) ptr)^ = cast(uint) bounded_uint(value, cast(u128)max(uint) ) or_return
|
||||
case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) bounded_uint(value, cast(u128)max(uintptr)) or_return
|
||||
|
||||
case u16le: (cast(^u16le) ptr)^ = cast(u16le) bounded_uint(value, cast(u128)max(u16le) ) or_return
|
||||
case u32le: (cast(^u32le) ptr)^ = cast(u32le) bounded_uint(value, cast(u128)max(u32le) ) or_return
|
||||
case u64le: (cast(^u64le) ptr)^ = cast(u64le) bounded_uint(value, cast(u128)max(u64le) ) or_return
|
||||
case u128le: (cast(^u128le) ptr)^ = cast(u128le) bounded_uint(value, cast(u128)max(u128le) ) or_return
|
||||
|
||||
case u16be: (cast(^u16be) ptr)^ = cast(u16be) bounded_uint(value, cast(u128)max(u16be) ) or_return
|
||||
case u32be: (cast(^u32be) ptr)^ = cast(u32be) bounded_uint(value, cast(u128)max(u32be) ) or_return
|
||||
case u64be: (cast(^u64be) ptr)^ = cast(u64be) bounded_uint(value, cast(u128)max(u64be) ) or_return
|
||||
case u128be: (cast(^u128be) ptr)^ = cast(u128be) bounded_uint(value, cast(u128)max(u128be) ) or_return
|
||||
}
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Rune:
|
||||
if utf8.rune_count_in_string(str) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
(cast(^rune)ptr)^ = utf8.rune_at_pos(str, 0)
|
||||
|
||||
case runtime.Type_Info_Float:
|
||||
value := strconv.parse_f64(str) or_return
|
||||
switch type_info.id {
|
||||
case f16: (cast(^f16) ptr)^ = cast(f16) value
|
||||
case f32: (cast(^f32) ptr)^ = cast(f32) value
|
||||
case f64: (cast(^f64) ptr)^ = value
|
||||
|
||||
case f16le: (cast(^f16le)ptr)^ = cast(f16le) value
|
||||
case f32le: (cast(^f32le)ptr)^ = cast(f32le) value
|
||||
case f64le: (cast(^f64le)ptr)^ = cast(f64le) value
|
||||
|
||||
case f16be: (cast(^f16be)ptr)^ = cast(f16be) value
|
||||
case f32be: (cast(^f32be)ptr)^ = cast(f32be) value
|
||||
case f64be: (cast(^f64be)ptr)^ = cast(f64be) value
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Complex:
|
||||
value := strconv.parse_complex128(str) or_return
|
||||
switch type_info.id {
|
||||
case complex128: (cast(^complex128)ptr)^ = value
|
||||
case complex64: (cast(^complex64) ptr)^ = cast(complex64)value
|
||||
case complex32: (cast(^complex32) ptr)^ = cast(complex32)value
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Quaternion:
|
||||
value := strconv.parse_quaternion256(str) or_return
|
||||
switch type_info.id {
|
||||
case quaternion256: (cast(^quaternion256)ptr)^ = value
|
||||
case quaternion128: (cast(^quaternion128)ptr)^ = cast(quaternion128)value
|
||||
case quaternion64: (cast(^quaternion64) ptr)^ = cast(quaternion64)value
|
||||
}
|
||||
|
||||
case runtime.Type_Info_String:
|
||||
if specific_type_info.is_cstring {
|
||||
cstr_ptr := cast(^cstring)ptr
|
||||
if cstr_ptr != nil {
|
||||
// Prevent memory leaks from us setting this value multiple times.
|
||||
delete(cstr_ptr^)
|
||||
}
|
||||
cstr_ptr^ = strings.clone_to_cstring(str)
|
||||
} else {
|
||||
(cast(^string)ptr)^ = str
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Boolean:
|
||||
value := strconv.parse_bool(str) or_return
|
||||
switch type_info.id {
|
||||
case bool: (cast(^bool) ptr)^ = value
|
||||
case b8: (cast(^b8) ptr)^ = cast(b8) value
|
||||
case b16: (cast(^b16) ptr)^ = cast(b16) value
|
||||
case b32: (cast(^b32) ptr)^ = cast(b32) value
|
||||
case b64: (cast(^b64) ptr)^ = cast(b64) value
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Bit_Set:
|
||||
// Parse a string of 1's and 0's, from left to right,
|
||||
// least significant bit to most significant bit.
|
||||
value: u128
|
||||
|
||||
// NOTE: `upper` is inclusive, i.e: `0..=31`
|
||||
max_bit_index := cast(u128)(1 + specific_type_info.upper - specific_type_info.lower)
|
||||
bit_index : u128 = 0
|
||||
#no_bounds_check for string_index : uint = 0; string_index < len(str); string_index += 1 {
|
||||
if bit_index == max_bit_index {
|
||||
// The string's too long for this bit_set.
|
||||
return false
|
||||
}
|
||||
|
||||
switch str[string_index] {
|
||||
case '1':
|
||||
value |= 1 << bit_index
|
||||
bit_index += 1
|
||||
case '0':
|
||||
bit_index += 1
|
||||
continue
|
||||
case '_':
|
||||
continue
|
||||
case:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if specific_type_info.underlying != nil {
|
||||
set_unbounded_integer_by_type(ptr, value, specific_type_info.underlying.id)
|
||||
} else {
|
||||
switch 8*type_info.size {
|
||||
case 8: (cast(^u8) ptr)^ = cast(u8) value
|
||||
case 16: (cast(^u16) ptr)^ = cast(u16) value
|
||||
case 32: (cast(^u32) ptr)^ = cast(u32) value
|
||||
case 64: (cast(^u64) ptr)^ = cast(u64) value
|
||||
case 128: (cast(^u128) ptr)^ = cast(u128) value
|
||||
}
|
||||
}
|
||||
|
||||
case:
|
||||
fmt.panicf("Unsupported base data type: %v", specific_type_info)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// This proc exists to make error handling easier, since everything in the base
|
||||
// type one above works on booleans. It's a simple parsing error if it's false.
|
||||
//
|
||||
// However, here we have to be more careful about how we handle errors,
|
||||
// especially with files.
|
||||
//
|
||||
// We want to provide as informative as an error as we can.
|
||||
@(optimization_mode="size", disabled=NO_CORE_NAMED_TYPES)
|
||||
parse_and_set_pointer_by_named_type :: proc(ptr: rawptr, str: string, data_type: typeid, arg_tag: string, out_error: ^Error) {
|
||||
// Core types currently supported:
|
||||
//
|
||||
// - os.Handle
|
||||
// - time.Time
|
||||
// - datetime.DateTime
|
||||
// - net.Host_Or_Endpoint
|
||||
|
||||
GENERIC_RFC_3339_ERROR :: "Invalid RFC 3339 string. Try this format: `yyyy-mm-ddThh:mm:ssZ`, for example `2024-02-29T16:30:00Z`."
|
||||
|
||||
out_error^ = nil
|
||||
|
||||
if data_type == os.Handle {
|
||||
// NOTE: `os` is hopefully available everywhere, even if it might panic on some calls.
|
||||
wants_read := false
|
||||
wants_write := false
|
||||
mode: int
|
||||
|
||||
if file, ok := get_struct_subtag(arg_tag, SUBTAG_FILE); ok {
|
||||
for i := 0; i < len(file); i += 1 {
|
||||
#no_bounds_check switch file[i] {
|
||||
case 'r': wants_read = true
|
||||
case 'w': wants_write = true
|
||||
case 'c': mode |= os.O_CREATE
|
||||
case 'a': mode |= os.O_APPEND
|
||||
case 't': mode |= os.O_TRUNC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sane default.
|
||||
// owner/group/other: r--r--r--
|
||||
perms: int = 0o444
|
||||
|
||||
if wants_read && wants_write {
|
||||
mode |= os.O_RDWR
|
||||
perms |= 0o200
|
||||
} else if wants_write {
|
||||
mode |= os.O_WRONLY
|
||||
perms |= 0o200
|
||||
} else {
|
||||
mode |= os.O_RDONLY
|
||||
}
|
||||
|
||||
if permstr, ok := get_struct_subtag(arg_tag, SUBTAG_PERMS); ok {
|
||||
if value, parse_ok := strconv.parse_u64_of_base(permstr, 8); parse_ok {
|
||||
perms = cast(int)value
|
||||
}
|
||||
}
|
||||
|
||||
handle, errno := os.open(str, mode, perms)
|
||||
if errno != 0 {
|
||||
// NOTE(Feoramund): os.Errno is system-dependent, and there's
|
||||
// currently no good way to translate them all into strings.
|
||||
//
|
||||
// The upcoming `os2` package will hopefully solve this.
|
||||
//
|
||||
// We can at least provide the number for now, so the user can look
|
||||
// it up.
|
||||
out_error^ = Open_File_Error {
|
||||
str,
|
||||
errno,
|
||||
mode,
|
||||
perms,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
(cast(^os.Handle)ptr)^ = handle
|
||||
return
|
||||
}
|
||||
|
||||
when IMPORTING_TIME {
|
||||
if data_type == time.Time {
|
||||
// NOTE: The leap second data is discarded.
|
||||
res, consumed := time.rfc3339_to_time_utc(str)
|
||||
if consumed == 0 {
|
||||
// The RFC 3339 parsing facilities provide no indication as to what
|
||||
// went wrong, so just treat it as a regular parsing error.
|
||||
out_error^ = Parse_Error {
|
||||
.Bad_Value,
|
||||
GENERIC_RFC_3339_ERROR,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
(cast(^time.Time)ptr)^ = res
|
||||
return
|
||||
} else if data_type == datetime.DateTime {
|
||||
// NOTE: The UTC offset and leap second data are discarded.
|
||||
res, _, _, consumed := time.rfc3339_to_components(str)
|
||||
if consumed == 0 {
|
||||
out_error^ = Parse_Error {
|
||||
.Bad_Value,
|
||||
GENERIC_RFC_3339_ERROR,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
(cast(^datetime.DateTime)ptr)^ = res
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
when IMPORTING_NET {
|
||||
if try_net_parse_workaround(data_type, str, ptr, out_error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
out_error ^= Parse_Error {
|
||||
// The caller will add more details.
|
||||
.Unsupported_Type,
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
@(optimization_mode="size")
|
||||
set_unbounded_integer_by_type :: proc(ptr: rawptr, value: $T, data_type: typeid) where intrinsics.type_is_integer(T) {
|
||||
switch data_type {
|
||||
case i8: (cast(^i8) ptr)^ = cast(i8) value
|
||||
case i16: (cast(^i16) ptr)^ = cast(i16) value
|
||||
case i32: (cast(^i32) ptr)^ = cast(i32) value
|
||||
case i64: (cast(^i64) ptr)^ = cast(i64) value
|
||||
case i128: (cast(^i128) ptr)^ = cast(i128) value
|
||||
|
||||
case int: (cast(^int) ptr)^ = cast(int) value
|
||||
|
||||
case i16le: (cast(^i16le) ptr)^ = cast(i16le) value
|
||||
case i32le: (cast(^i32le) ptr)^ = cast(i32le) value
|
||||
case i64le: (cast(^i64le) ptr)^ = cast(i64le) value
|
||||
case i128le: (cast(^i128le) ptr)^ = cast(i128le) value
|
||||
|
||||
case i16be: (cast(^i16be) ptr)^ = cast(i16be) value
|
||||
case i32be: (cast(^i32be) ptr)^ = cast(i32be) value
|
||||
case i64be: (cast(^i64be) ptr)^ = cast(i64be) value
|
||||
case i128be: (cast(^i128be) ptr)^ = cast(i128be) value
|
||||
|
||||
case u8: (cast(^u8) ptr)^ = cast(u8) value
|
||||
case u16: (cast(^u16) ptr)^ = cast(u16) value
|
||||
case u32: (cast(^u32) ptr)^ = cast(u32) value
|
||||
case u64: (cast(^u64) ptr)^ = cast(u64) value
|
||||
case u128: (cast(^u128) ptr)^ = cast(u128) value
|
||||
|
||||
case uint: (cast(^uint) ptr)^ = cast(uint) value
|
||||
case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) value
|
||||
|
||||
case u16le: (cast(^u16le) ptr)^ = cast(u16le) value
|
||||
case u32le: (cast(^u32le) ptr)^ = cast(u32le) value
|
||||
case u64le: (cast(^u64le) ptr)^ = cast(u64le) value
|
||||
case u128le: (cast(^u128le) ptr)^ = cast(u128le) value
|
||||
|
||||
case u16be: (cast(^u16be) ptr)^ = cast(u16be) value
|
||||
case u32be: (cast(^u32be) ptr)^ = cast(u32be) value
|
||||
case u64be: (cast(^u64be) ptr)^ = cast(u64be) value
|
||||
case u128be: (cast(^u128be) ptr)^ = cast(u128be) value
|
||||
|
||||
case rune: (cast(^rune) ptr)^ = cast(rune) value
|
||||
|
||||
case:
|
||||
fmt.panicf("Unsupported integer backing type: %v", data_type)
|
||||
}
|
||||
}
|
||||
|
||||
@(optimization_mode="size")
|
||||
parse_and_set_pointer_by_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info, arg_tag: string) -> (error: Error) {
|
||||
#partial switch specific_type_info in type_info.variant {
|
||||
case runtime.Type_Info_Named:
|
||||
if global_custom_type_setter != nil {
|
||||
// The program gets to go first.
|
||||
error_message, handled, alloc_error := global_custom_type_setter(ptr, type_info.id, str, arg_tag)
|
||||
|
||||
if alloc_error != nil {
|
||||
// There was an allocation error. Bail out.
|
||||
return Parse_Error {
|
||||
alloc_error,
|
||||
"Custom type setter encountered allocation error.",
|
||||
}
|
||||
}
|
||||
|
||||
if handled {
|
||||
// The program handled the type.
|
||||
|
||||
if len(error_message) != 0 {
|
||||
// However, there was an error. Pass it along.
|
||||
error = Parse_Error {
|
||||
.Bad_Value,
|
||||
error_message,
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Might be a named enum. Need to check here first, since we handle all enums.
|
||||
if enum_type_info, is_enum := specific_type_info.base.variant.(runtime.Type_Info_Enum); is_enum {
|
||||
if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
|
||||
set_unbounded_integer_by_type(ptr, value, enum_type_info.base.id)
|
||||
} else {
|
||||
return Parse_Error {
|
||||
.Bad_Value,
|
||||
fmt.tprintf("Invalid value name. Valid names are: %s", enum_type_info.names),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parse_and_set_pointer_by_named_type(ptr, str, type_info.id, arg_tag, &error)
|
||||
|
||||
if error != nil {
|
||||
// So far, it's none of the types that we recognize.
|
||||
// Check to see if we can set it by base type, if allowed.
|
||||
if _, is_indistinct := get_struct_subtag(arg_tag, SUBTAG_INDISTINCT); is_indistinct {
|
||||
return parse_and_set_pointer_by_type(ptr, str, specific_type_info.base, arg_tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
ptr := cast(^runtime.Raw_Dynamic_Array)ptr
|
||||
|
||||
// Try to convert the value first.
|
||||
elem_backing, alloc_error := mem.alloc_bytes(specific_type_info.elem.size, specific_type_info.elem.align)
|
||||
if alloc_error != nil {
|
||||
return Parse_Error {
|
||||
alloc_error,
|
||||
"Failed to allocate element backing for dynamic array.",
|
||||
}
|
||||
}
|
||||
defer delete(elem_backing)
|
||||
parse_and_set_pointer_by_type(raw_data(elem_backing), str, specific_type_info.elem, arg_tag) or_return
|
||||
|
||||
if !runtime.__dynamic_array_resize(ptr, specific_type_info.elem.size, specific_type_info.elem.align, ptr.len + 1) {
|
||||
// NOTE: This is purely an assumption that it's OOM.
|
||||
// Regardless, the resize failed.
|
||||
return Parse_Error {
|
||||
runtime.Allocator_Error.Out_Of_Memory,
|
||||
"Failed to resize dynamic array.",
|
||||
}
|
||||
}
|
||||
|
||||
subptr := cast(rawptr)(
|
||||
cast(uintptr)ptr.data +
|
||||
cast(uintptr)((ptr.len - 1) * specific_type_info.elem.size))
|
||||
mem.copy(subptr, raw_data(elem_backing), len(elem_backing))
|
||||
|
||||
case runtime.Type_Info_Enum:
|
||||
// This is a nameless enum.
|
||||
// The code here is virtually the same as above for named enums.
|
||||
if value, ok := reflect.enum_from_name_any(type_info.id, str); ok {
|
||||
set_unbounded_integer_by_type(ptr, value, specific_type_info.base.id)
|
||||
} else {
|
||||
return Parse_Error {
|
||||
.Bad_Value,
|
||||
fmt.tprintf("Invalid value name. Valid names are: %s", specific_type_info.names),
|
||||
}
|
||||
}
|
||||
|
||||
case:
|
||||
if !parse_and_set_pointer_by_base_type(ptr, str, type_info) {
|
||||
return Parse_Error {
|
||||
// The caller will add more details.
|
||||
.Bad_Value,
|
||||
"",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
get_struct_subtag :: get_subtag
|
||||
|
||||
get_field_name :: proc(field: reflect.Struct_Field) -> string {
|
||||
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
||||
if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok {
|
||||
return name_subtag
|
||||
}
|
||||
}
|
||||
|
||||
name, _ := strings.replace_all(field.name, "_", "-", context.temp_allocator)
|
||||
return name
|
||||
}
|
||||
|
||||
get_field_pos :: proc(field: reflect.Struct_Field) -> (int, bool) {
|
||||
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
||||
if pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS); pos_ok {
|
||||
if value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10); parse_ok {
|
||||
return cast(int)value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Get a struct field by its field name or `name` subtag.
|
||||
get_field_by_name :: proc(model: ^$T, name: string) -> (result: reflect.Struct_Field, index: int, error: Error) {
|
||||
for field, i in reflect.struct_fields_zipped(T) {
|
||||
if get_field_name(field) == name {
|
||||
return field, i, nil
|
||||
}
|
||||
}
|
||||
|
||||
error = Parse_Error {
|
||||
.Missing_Flag,
|
||||
fmt.tprintf("Unable to find any flag named `%s`.", name),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get a struct field by its `pos` subtag.
|
||||
get_field_by_pos :: proc(model: ^$T, pos: int) -> (result: reflect.Struct_Field, index: int, ok: bool) {
|
||||
for field, i in reflect.struct_fields_zipped(T) {
|
||||
args_tag, tag_ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
|
||||
if !tag_ok {
|
||||
continue
|
||||
}
|
||||
|
||||
pos_subtag, pos_ok := get_struct_subtag(args_tag, SUBTAG_POS)
|
||||
if !pos_ok {
|
||||
continue
|
||||
}
|
||||
|
||||
value, parse_ok := strconv.parse_u64_of_base(pos_subtag, 10)
|
||||
if parse_ok && cast(int)value == pos {
|
||||
return field, i, true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//+private
|
||||
//+build !freebsd !netbsd !openbsd
|
||||
package flags
|
||||
|
||||
import "core:net"
|
||||
|
||||
// This proc exists purely as a workaround for import restrictions.
|
||||
// Returns true if caller should return early.
|
||||
try_net_parse_workaround :: #force_inline proc (
|
||||
data_type: typeid,
|
||||
str: string,
|
||||
ptr: rawptr,
|
||||
out_error: ^Error,
|
||||
) -> bool {
|
||||
if data_type == net.Host_Or_Endpoint {
|
||||
addr, net_error := net.parse_hostname_or_endpoint(str)
|
||||
if net_error != nil {
|
||||
// We pass along `net.Error` here.
|
||||
out_error^ = Parse_Error {
|
||||
net_error,
|
||||
"Invalid Host/Endpoint.",
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
(cast(^net.Host_Or_Endpoint)ptr)^ = addr
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//+private
|
||||
package flags
|
||||
|
||||
@require import "base:runtime"
|
||||
@require import "core:container/bit_array"
|
||||
@require import "core:fmt"
|
||||
@require import "core:mem"
|
||||
@require import "core:os"
|
||||
@require import "core:reflect"
|
||||
@require import "core:strconv"
|
||||
@require import "core:strings"
|
||||
|
||||
// This proc is used to assert that `T` meets the expectations of the library.
|
||||
@(optimization_mode="size", disabled=ODIN_DISABLE_ASSERT)
|
||||
validate_structure :: proc(model_type: $T, style: Parsing_Style, loc := #caller_location) {
|
||||
positionals_assigned_so_far: bit_array.Bit_Array
|
||||
defer bit_array.destroy(&positionals_assigned_so_far)
|
||||
|
||||
check_fields: for field in reflect.struct_fields_zipped(T) {
|
||||
if style == .Unix {
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
fmt.panicf("%T.%s is a map type, and these are not supported in UNIX-style parsing mode.",
|
||||
model_type, field.name, loc = loc)
|
||||
}
|
||||
}
|
||||
|
||||
name_is_safe := true
|
||||
defer {
|
||||
fmt.assertf(name_is_safe, "%T.%s is using a reserved name.",
|
||||
model_type, field.name, loc = loc)
|
||||
}
|
||||
|
||||
switch field.name {
|
||||
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
|
||||
name_is_safe = false
|
||||
}
|
||||
|
||||
args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
|
||||
if !ok {
|
||||
// If it has no args tag, then we've checked all we need to.
|
||||
// Most of this proc is validating that the subtags are sane.
|
||||
continue
|
||||
}
|
||||
|
||||
if name, has_name := get_struct_subtag(args_tag, SUBTAG_NAME); has_name {
|
||||
fmt.assertf(len(name) > 0, "%T.%s has a zero-length `%s`.",
|
||||
model_type, field.name, SUBTAG_NAME, loc = loc)
|
||||
|
||||
fmt.assertf(strings.index(name, " ") == -1, "%T.%s has a `%s` with spaces in it.",
|
||||
model_type, field.name, SUBTAG_NAME, loc = loc)
|
||||
|
||||
switch name {
|
||||
case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
|
||||
name_is_safe = false
|
||||
continue check_fields
|
||||
case:
|
||||
name_is_safe = true
|
||||
}
|
||||
}
|
||||
|
||||
if pos_str, has_pos := get_struct_subtag(args_tag, SUBTAG_POS); has_pos {
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
fmt.panicf("%T.%s has `%s` defined, and this does not make sense on a map type.",
|
||||
model_type, field.name, SUBTAG_POS, loc = loc)
|
||||
}
|
||||
|
||||
pos_value, pos_ok := strconv.parse_u64_of_base(pos_str, 10)
|
||||
fmt.assertf(pos_ok, "%T.%s has `%s` defined as %q but cannot be parsed a base-10 integer >= 0.",
|
||||
model_type, field.name, SUBTAG_POS, pos_str, loc = loc)
|
||||
fmt.assertf(!bit_array.get(&positionals_assigned_so_far, pos_value), "%T.%s has `%s` set to #%i, but that position has already been assigned to another flag.",
|
||||
model_type, field.name, SUBTAG_POS, pos_value, loc = loc)
|
||||
bit_array.set(&positionals_assigned_so_far, pos_value)
|
||||
}
|
||||
|
||||
required_min, required_max: int
|
||||
if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
|
||||
fmt.assertf(!reflect.is_boolean(field.type), "%T.%s is a required boolean. This is disallowed.",
|
||||
model_type, field.name, loc = loc)
|
||||
|
||||
fmt.assertf(field.name != INTERNAL_VARIADIC_FLAG, "%T.%s is defined as required. This is disallowed.",
|
||||
model_type, field.name, loc = loc)
|
||||
|
||||
if len(requirement) > 0 {
|
||||
if required_min, required_max, ok = parse_requirements(requirement); ok {
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
fmt.assertf(required_min != required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are the same. Increase the maximum by 1 for an exact number of arguments: (%i<%i)",
|
||||
model_type,
|
||||
field.name,
|
||||
SUBTAG_REQUIRED,
|
||||
requirement,
|
||||
required_min,
|
||||
1 + required_max,
|
||||
loc = loc)
|
||||
|
||||
fmt.assertf(required_min < required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are swapped.",
|
||||
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
|
||||
|
||||
case:
|
||||
fmt.panicf("%T.%s has `%s` defined as %q, but ranges are only supported on dynamic arrays.",
|
||||
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
|
||||
}
|
||||
} else {
|
||||
fmt.panicf("%T.%s has `%s` defined as %q, but it cannot be parsed as a valid range.",
|
||||
model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if length, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
|
||||
if value, parse_ok := strconv.parse_u64_of_base(length, 10); parse_ok {
|
||||
fmt.assertf(value > 0,
|
||||
"%T.%s has `%s` set to %i. It must be greater than zero.",
|
||||
model_type, field.name, value, SUBTAG_VARIADIC, loc = loc)
|
||||
fmt.assertf(value != 1,
|
||||
"%T.%s has `%s` set to 1. This has no effect.",
|
||||
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
|
||||
}
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
fmt.assertf(style != .Odin,
|
||||
"%T.%s has `%s` defined, but this only makes sense in UNIX-style parsing mode.",
|
||||
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
|
||||
case:
|
||||
fmt.panicf("%T.%s has `%s` defined, but this only makes sense on dynamic arrays.",
|
||||
model_type, field.name, SUBTAG_VARIADIC, loc = loc)
|
||||
}
|
||||
}
|
||||
|
||||
allowed_to_define_file_perms: bool = ---
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
allowed_to_define_file_perms = specific_type_info.value.id == os.Handle
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
allowed_to_define_file_perms = specific_type_info.elem.id == os.Handle
|
||||
case:
|
||||
allowed_to_define_file_perms = field.type.id == os.Handle
|
||||
}
|
||||
|
||||
if _, has_file := get_struct_subtag(args_tag, SUBTAG_FILE); has_file {
|
||||
fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
|
||||
model_type, field.name, SUBTAG_FILE, loc = loc)
|
||||
}
|
||||
|
||||
if _, has_perms := get_struct_subtag(args_tag, SUBTAG_PERMS); has_perms {
|
||||
fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
|
||||
model_type, field.name, SUBTAG_PERMS, loc = loc)
|
||||
}
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
fmt.assertf(reflect.is_string(specific_type_info.key), "%T.%s is defined as a map[%T]. Only string types are currently supported as map keys.",
|
||||
model_type,
|
||||
field.name,
|
||||
specific_type_info.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all the required arguments are set and that the set arguments
|
||||
// are up to the program's expectations.
|
||||
@(optimization_mode="size")
|
||||
validate_arguments :: proc(model: ^$T, parser: ^Parser) -> Error {
|
||||
check_fields: for field, index in reflect.struct_fields_zipped(T) {
|
||||
was_set := bit_array.get(&parser.fields_set, index)
|
||||
|
||||
field_name := get_field_name(field)
|
||||
args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
|
||||
requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED)
|
||||
|
||||
required_min, required_max: int
|
||||
has_requirements: bool
|
||||
if is_required {
|
||||
required_min, required_max, has_requirements = parse_requirements(requirement)
|
||||
}
|
||||
|
||||
if has_requirements && required_min == 0 {
|
||||
// Allow `0<n` or `<n` to bypass the required condition.
|
||||
is_required = false
|
||||
}
|
||||
|
||||
if _, is_array := field.type.variant.(runtime.Type_Info_Dynamic_Array); is_array && has_requirements {
|
||||
// If it's an array, make sure it meets the required number of arguments.
|
||||
ptr := cast(^runtime.Raw_Dynamic_Array)(cast(uintptr)model + field.offset)
|
||||
if required_min == required_max - 1 && ptr.len != required_min {
|
||||
return Validation_Error {
|
||||
fmt.tprintf("The flag `%s` had %i option%s set, but it requires exactly %i.",
|
||||
field_name,
|
||||
ptr.len,
|
||||
"" if ptr.len == 1 else "s",
|
||||
required_min),
|
||||
}
|
||||
} else if required_min > ptr.len || ptr.len >= required_max {
|
||||
if required_max == max(int) {
|
||||
return Validation_Error {
|
||||
fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i.",
|
||||
field_name,
|
||||
ptr.len,
|
||||
"" if ptr.len == 1 else "s",
|
||||
required_min),
|
||||
}
|
||||
} else {
|
||||
return Validation_Error {
|
||||
fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i and at most %i.",
|
||||
field_name,
|
||||
ptr.len,
|
||||
"" if ptr.len == 1 else "s",
|
||||
required_min,
|
||||
required_max - 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !was_set {
|
||||
if is_required {
|
||||
return Validation_Error {
|
||||
fmt.tprintf("The required flag `%s` was not set.", field_name),
|
||||
}
|
||||
}
|
||||
|
||||
// Not set, not required; moving on.
|
||||
continue
|
||||
}
|
||||
|
||||
// All default checks have passed. The program gets a look at it now.
|
||||
|
||||
if global_custom_flag_checker != nil {
|
||||
ptr := cast(rawptr)(cast(uintptr)model + field.offset)
|
||||
error := global_custom_flag_checker(model,
|
||||
field.name,
|
||||
mem.make_any(ptr, field.type.id),
|
||||
args_tag)
|
||||
|
||||
if len(error) > 0 {
|
||||
// The program reported an error message.
|
||||
return Validation_Error { error }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package flags
|
||||
|
||||
@require import "core:container/bit_array"
|
||||
@require import "core:fmt"
|
||||
|
||||
Parsing_Style :: enum {
|
||||
// Odin-style: `-flag`, `-flag:option`, `-map:key=value`
|
||||
Odin,
|
||||
// UNIX-style: `-flag` or `--flag`, `--flag=argument`, `--flag argument repeating-argument`
|
||||
Unix,
|
||||
}
|
||||
|
||||
/*
|
||||
Parse a slice of command-line arguments into an annotated struct.
|
||||
|
||||
*Allocates Using Provided Allocator*
|
||||
|
||||
By default, this proc will only allocate memory outside of its lifetime if it
|
||||
has to append to a dynamic array, set a map value, or set a cstring.
|
||||
|
||||
The program is expected to free any allocations on `model` as a result of parsing.
|
||||
|
||||
Inputs:
|
||||
- model: A pointer to an annotated struct with flag definitions.
|
||||
- args: A slice of strings, usually `os.args[1:]`.
|
||||
- style: The argument parsing style.
|
||||
- validate_args: If `true`, will ensure that all required arguments are set if no errors occurred.
|
||||
- strict: If `true`, will return on first error. Otherwise, parsing continues.
|
||||
- allocator: (default: context.allocator)
|
||||
- loc: The caller location for debugging purposes (default: #caller_location)
|
||||
|
||||
Returns:
|
||||
- error: A union of errors; parsing, file open, a help request, or validation.
|
||||
*/
|
||||
@(optimization_mode="size")
|
||||
parse :: proc(
|
||||
model: ^$T,
|
||||
args: []string,
|
||||
style: Parsing_Style = .Odin,
|
||||
validate_args: bool = true,
|
||||
strict: bool = true,
|
||||
allocator := context.allocator,
|
||||
loc := #caller_location,
|
||||
) -> (error: Error) {
|
||||
context.allocator = allocator
|
||||
validate_structure(model^, style, loc)
|
||||
|
||||
parser: Parser
|
||||
defer {
|
||||
bit_array.destroy(&parser.filled_pos)
|
||||
bit_array.destroy(&parser.fields_set)
|
||||
}
|
||||
|
||||
switch style {
|
||||
case .Odin:
|
||||
for arg in args {
|
||||
error = parse_one_odin_arg(model, &parser, arg)
|
||||
if strict && error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case .Unix:
|
||||
// Support for `-flag argument (repeating-argument ...)`
|
||||
future_args: int
|
||||
current_flag: string
|
||||
|
||||
for i := 0; i < len(args); i += 1 {
|
||||
#no_bounds_check arg := args[i]
|
||||
future_args, current_flag, error = parse_one_unix_arg(model, &parser, arg)
|
||||
if strict && error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for starting_future_args := future_args; future_args > 0; future_args -= 1 {
|
||||
i += 1
|
||||
if i == len(args) {
|
||||
if future_args == starting_future_args {
|
||||
return Parse_Error {
|
||||
.No_Value,
|
||||
fmt.tprintf("Expected a value for `%s` but none was given.", current_flag),
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
#no_bounds_check arg = args[i]
|
||||
|
||||
error = set_option(model, &parser, current_flag, arg)
|
||||
if strict && error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if error == nil && validate_args {
|
||||
return validate_arguments(model, &parser)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
/*
|
||||
Handle setting custom data types.
|
||||
|
||||
Inputs:
|
||||
- data: A raw pointer to the field where the data will go.
|
||||
- data_type: Type information on the underlying field.
|
||||
- unparsed_value: The unparsed string that the flag is being set to.
|
||||
- args_tag: The `args` tag from the struct's field.
|
||||
|
||||
Returns:
|
||||
- error: An error message, or an empty string if no error occurred.
|
||||
- handled: A boolean indicating if the setter handles this type.
|
||||
- alloc_error: If an allocation error occurred, return it here.
|
||||
*/
|
||||
Custom_Type_Setter :: #type proc(
|
||||
data: rawptr,
|
||||
data_type: typeid,
|
||||
unparsed_value: string,
|
||||
args_tag: string,
|
||||
) -> (
|
||||
error: string,
|
||||
handled: bool,
|
||||
alloc_error: runtime.Allocator_Error,
|
||||
)
|
||||
|
||||
@(private)
|
||||
global_custom_type_setter: Custom_Type_Setter
|
||||
|
||||
/*
|
||||
Set the global custom type setter.
|
||||
|
||||
Note that only one can be active at a time.
|
||||
|
||||
Inputs:
|
||||
- setter: The type setter. Pass `nil` to disable any previously set setter.
|
||||
*/
|
||||
register_type_setter :: proc(setter: Custom_Type_Setter) {
|
||||
global_custom_type_setter = setter
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package flags
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:reflect"
|
||||
import "core:slice"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
|
||||
/*
|
||||
Write out the documentation for the command-line arguments to a stream.
|
||||
|
||||
Inputs:
|
||||
- out: The stream to write to.
|
||||
- data_type: The typeid of the data structure to describe.
|
||||
- program: The name of the program, usually the first argument to `os.args`.
|
||||
- style: The argument parsing style, required to show flags in the proper style.
|
||||
*/
|
||||
@(optimization_mode="size")
|
||||
write_usage :: proc(out: io.Writer, data_type: typeid, program: string = "", style: Parsing_Style = .Odin) {
|
||||
// All flags get their tags parsed so they can be reasoned about later.
|
||||
Flag :: struct {
|
||||
name: string,
|
||||
usage: string,
|
||||
type_description: string,
|
||||
full_length: int,
|
||||
pos: int,
|
||||
required_min, required_max: int,
|
||||
is_positional: bool,
|
||||
is_required: bool,
|
||||
is_boolean: bool,
|
||||
is_variadic: bool,
|
||||
variadic_length: int,
|
||||
}
|
||||
|
||||
//
|
||||
// POSITIONAL+REQUIRED, POSITIONAL, REQUIRED, NON_REQUIRED+NON_POSITIONAL, ...
|
||||
//
|
||||
sort_flags :: proc(i, j: Flag) -> slice.Ordering {
|
||||
// `varg` goes to the end.
|
||||
if i.name == INTERNAL_VARIADIC_FLAG {
|
||||
return .Greater
|
||||
} else if j.name == INTERNAL_VARIADIC_FLAG {
|
||||
return .Less
|
||||
}
|
||||
|
||||
// Handle positionals.
|
||||
if i.is_positional {
|
||||
if j.is_positional {
|
||||
return slice.cmp(i.pos, j.pos)
|
||||
} else {
|
||||
return .Less
|
||||
}
|
||||
} else {
|
||||
if j.is_positional {
|
||||
return .Greater
|
||||
}
|
||||
}
|
||||
|
||||
// Then required flags.
|
||||
if i.is_required {
|
||||
if !j.is_required {
|
||||
return .Less
|
||||
}
|
||||
} else if j.is_required {
|
||||
return .Greater
|
||||
}
|
||||
|
||||
// Finally, sort by name.
|
||||
return slice.cmp(i.name, j.name)
|
||||
}
|
||||
|
||||
describe_array_requirements :: proc(flag: Flag) -> (spec: string) {
|
||||
if flag.is_required {
|
||||
if flag.required_min == flag.required_max - 1 {
|
||||
spec = fmt.tprintf(", exactly %i", flag.required_min)
|
||||
} else if flag.required_min > 0 && flag.required_max == max(int) {
|
||||
spec = fmt.tprintf(", at least %i", flag.required_min)
|
||||
} else if flag.required_min == 0 && flag.required_max > 1 {
|
||||
spec = fmt.tprintf(", at most %i", flag.required_max - 1)
|
||||
} else if flag.required_min > 0 && flag.required_max > 1 {
|
||||
spec = fmt.tprintf(", between %i and %i", flag.required_min, flag.required_max - 1)
|
||||
} else {
|
||||
spec = ", required"
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
builder := strings.builder_make()
|
||||
defer strings.builder_destroy(&builder)
|
||||
|
||||
flag_prefix, flag_assignment: string = ---, ---
|
||||
switch style {
|
||||
case .Odin: flag_prefix = "-"; flag_assignment = ":"
|
||||
case .Unix: flag_prefix = "--"; flag_assignment = " "
|
||||
}
|
||||
|
||||
visible_flags: [dynamic]Flag
|
||||
defer delete(visible_flags)
|
||||
|
||||
longest_flag_length: int
|
||||
|
||||
for field in reflect.struct_fields_zipped(data_type) {
|
||||
flag: Flag
|
||||
|
||||
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
||||
if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
|
||||
// Hidden flags stay hidden.
|
||||
continue
|
||||
}
|
||||
if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
|
||||
flag.is_positional = true
|
||||
if pos, parse_ok := strconv.parse_u64_of_base(pos_str, 10); parse_ok {
|
||||
flag.pos = cast(int)pos
|
||||
}
|
||||
}
|
||||
if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
|
||||
flag.is_required = true
|
||||
flag.required_min, flag.required_max, _ = parse_requirements(requirement)
|
||||
}
|
||||
if length_str, is_variadic := get_struct_subtag(args_tag, SUBTAG_VARIADIC); is_variadic {
|
||||
flag.is_variadic = true
|
||||
if length, parse_ok := strconv.parse_u64_of_base(length_str, 10); parse_ok {
|
||||
flag.variadic_length = cast(int)length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flag.name = get_field_name(field)
|
||||
flag.is_boolean = reflect.is_boolean(field.type)
|
||||
|
||||
if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
|
||||
flag.usage = usage
|
||||
} else {
|
||||
flag.usage = UNDOCUMENTED_FLAG
|
||||
}
|
||||
|
||||
#partial switch specific_type_info in field.type.variant {
|
||||
case runtime.Type_Info_Map:
|
||||
flag.type_description = fmt.tprintf("<%v>=<%v>%s",
|
||||
specific_type_info.key.id,
|
||||
specific_type_info.value.id,
|
||||
", required" if flag.is_required else "")
|
||||
|
||||
case runtime.Type_Info_Dynamic_Array:
|
||||
requirement_spec := describe_array_requirements(flag)
|
||||
|
||||
if flag.is_variadic || flag.name == INTERNAL_VARIADIC_FLAG {
|
||||
if flag.variadic_length == 0 {
|
||||
flag.type_description = fmt.tprintf("<%v, ...>%s",
|
||||
specific_type_info.elem.id,
|
||||
requirement_spec)
|
||||
} else {
|
||||
flag.type_description = fmt.tprintf("<%v, %i at once>%s",
|
||||
specific_type_info.elem.id,
|
||||
flag.variadic_length,
|
||||
requirement_spec)
|
||||
}
|
||||
} else {
|
||||
flag.type_description = fmt.tprintf("<%v>%s", specific_type_info.elem.id,
|
||||
requirement_spec if len(requirement_spec) > 0 else ", multiple")
|
||||
}
|
||||
|
||||
case:
|
||||
if flag.is_boolean {
|
||||
/*
|
||||
if flag.is_required {
|
||||
flag.type_description = ", required"
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
flag.type_description = fmt.tprintf("<%v>%s",
|
||||
field.type.id,
|
||||
", required" if flag.is_required else "")
|
||||
}
|
||||
}
|
||||
|
||||
if flag.name == INTERNAL_VARIADIC_FLAG {
|
||||
flag.full_length = len(flag.type_description)
|
||||
} else if flag.is_boolean {
|
||||
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag.type_description)
|
||||
} else {
|
||||
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag_assignment) + len(flag.type_description)
|
||||
}
|
||||
|
||||
longest_flag_length = max(longest_flag_length, flag.full_length)
|
||||
|
||||
append(&visible_flags, flag)
|
||||
}
|
||||
|
||||
slice.sort_by_cmp(visible_flags[:], sort_flags)
|
||||
|
||||
// All the flags have been figured out now.
|
||||
|
||||
if len(program) > 0 {
|
||||
keep_it_short := len(visible_flags) >= ONE_LINE_FLAG_CUTOFF_COUNT
|
||||
|
||||
strings.write_string(&builder, "Usage:\n\t")
|
||||
strings.write_string(&builder, program)
|
||||
|
||||
for flag in visible_flags {
|
||||
if keep_it_short && !(flag.is_required || flag.is_positional || flag.name == INTERNAL_VARIADIC_FLAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
strings.write_byte(&builder, ' ')
|
||||
|
||||
if flag.name == INTERNAL_VARIADIC_FLAG {
|
||||
strings.write_string(&builder, "...")
|
||||
continue
|
||||
}
|
||||
|
||||
if !flag.is_required { strings.write_byte(&builder, '[') }
|
||||
if !flag.is_positional { strings.write_string(&builder, flag_prefix) }
|
||||
strings.write_string(&builder, flag.name)
|
||||
if !flag.is_required { strings.write_byte(&builder, ']') }
|
||||
}
|
||||
|
||||
strings.write_byte(&builder, '\n')
|
||||
}
|
||||
|
||||
if len(visible_flags) == 0 {
|
||||
// No visible flags. An unusual situation, but prevent any extra work.
|
||||
fmt.wprint(out, strings.to_string(builder))
|
||||
return
|
||||
}
|
||||
|
||||
strings.write_string(&builder, "Flags:\n")
|
||||
|
||||
// Divide the positional/required arguments and the non-required arguments.
|
||||
divider_index := -1
|
||||
for flag, i in visible_flags {
|
||||
if !flag.is_positional && !flag.is_required {
|
||||
divider_index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if divider_index == 0 {
|
||||
divider_index = -1
|
||||
}
|
||||
|
||||
for flag, i in visible_flags {
|
||||
if i == divider_index {
|
||||
SPACING :: 2 // Number of spaces before the '|' from below.
|
||||
strings.write_byte(&builder, '\t')
|
||||
spacing := strings.repeat(" ", SPACING + longest_flag_length, context.temp_allocator)
|
||||
strings.write_string(&builder, spacing)
|
||||
strings.write_string(&builder, "|\n")
|
||||
}
|
||||
|
||||
strings.write_byte(&builder, '\t')
|
||||
|
||||
if flag.name == INTERNAL_VARIADIC_FLAG {
|
||||
strings.write_string(&builder, flag.type_description)
|
||||
} else {
|
||||
strings.write_string(&builder, flag_prefix)
|
||||
strings.write_string(&builder, flag.name)
|
||||
if !flag.is_boolean {
|
||||
strings.write_string(&builder, flag_assignment)
|
||||
}
|
||||
strings.write_string(&builder, flag.type_description)
|
||||
}
|
||||
|
||||
if strings.contains_rune(flag.usage, '\n') {
|
||||
// Multi-line usage documentation. Let's make it look nice.
|
||||
usage_builder := strings.builder_make(context.temp_allocator)
|
||||
|
||||
strings.write_byte(&usage_builder, '\n')
|
||||
iter := strings.trim_space(flag.usage)
|
||||
for line in strings.split_lines_iterator(&iter) {
|
||||
strings.write_string(&usage_builder, "\t\t")
|
||||
strings.write_string(&usage_builder, strings.trim_left_space(line))
|
||||
strings.write_byte(&usage_builder, '\n')
|
||||
}
|
||||
|
||||
strings.write_string(&builder, strings.to_string(usage_builder))
|
||||
} else {
|
||||
// Single-line usage documentation.
|
||||
spacing := strings.repeat(" ",
|
||||
(longest_flag_length) - flag.full_length,
|
||||
context.temp_allocator)
|
||||
|
||||
strings.write_string(&builder, spacing)
|
||||
strings.write_string(&builder, " | ")
|
||||
strings.write_string(&builder, flag.usage)
|
||||
strings.write_byte(&builder, '\n')
|
||||
}
|
||||
}
|
||||
|
||||
fmt.wprint(out, strings.to_string(builder))
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package flags
|
||||
|
||||
import "core:fmt"
|
||||
@require import "core:os"
|
||||
@require import "core:path/filepath"
|
||||
import "core:strings"
|
||||
|
||||
/*
|
||||
Parse any arguments into an annotated struct or exit if there was an error.
|
||||
|
||||
*Allocates Using Provided Allocator*
|
||||
|
||||
This is a convenience wrapper over `parse` and `print_errors`.
|
||||
|
||||
Inputs:
|
||||
- model: A pointer to an annotated struct.
|
||||
- program_args: A slice of strings, usually `os.args`.
|
||||
- style: The argument parsing style.
|
||||
- allocator: (default: context.allocator)
|
||||
- loc: The caller location for debugging purposes (default: #caller_location)
|
||||
*/
|
||||
@(optimization_mode="size")
|
||||
parse_or_exit :: proc(
|
||||
model: ^$T,
|
||||
program_args: []string,
|
||||
style: Parsing_Style = .Odin,
|
||||
allocator := context.allocator,
|
||||
loc := #caller_location,
|
||||
) {
|
||||
assert(len(program_args) > 0, "Program arguments slice is empty.", loc)
|
||||
|
||||
program := filepath.base(program_args[0])
|
||||
args: []string
|
||||
|
||||
if len(program_args) > 1 {
|
||||
args = program_args[1:]
|
||||
}
|
||||
|
||||
error := parse(model, args, style)
|
||||
if error != nil {
|
||||
stderr := os.stream_from_handle(os.stderr)
|
||||
|
||||
if len(args) == 0 {
|
||||
// No arguments entered, and there was an error; show the usage,
|
||||
// specifically on STDERR.
|
||||
write_usage(stderr, T, program, style)
|
||||
fmt.wprintln(stderr)
|
||||
}
|
||||
|
||||
print_errors(T, error, program, style)
|
||||
|
||||
_, was_help_request := error.(Help_Request)
|
||||
os.exit(0 if was_help_request else 1)
|
||||
}
|
||||
}
|
||||
/*
|
||||
Print out any errors that may have resulted from parsing.
|
||||
|
||||
All error messages print to STDERR, while usage goes to STDOUT, if requested.
|
||||
|
||||
Inputs:
|
||||
- data_type: The typeid of the data structure to describe, if usage is requested.
|
||||
- error: The error returned from `parse`.
|
||||
- style: The argument parsing style, required to show flags in the proper style, when usage is shown.
|
||||
*/
|
||||
@(optimization_mode="size")
|
||||
print_errors :: proc(data_type: typeid, error: Error, program: string, style: Parsing_Style = .Odin) {
|
||||
stderr := os.stream_from_handle(os.stderr)
|
||||
stdout := os.stream_from_handle(os.stdout)
|
||||
|
||||
switch specific_error in error {
|
||||
case Parse_Error:
|
||||
fmt.wprintfln(stderr, "[%T.%v] %s", specific_error, specific_error.reason, specific_error.message)
|
||||
case Open_File_Error:
|
||||
fmt.wprintfln(stderr, "[%T#%i] Unable to open file with perms 0o%o in mode 0x%x: %s",
|
||||
specific_error,
|
||||
specific_error.errno,
|
||||
specific_error.perms,
|
||||
specific_error.mode,
|
||||
specific_error.filename)
|
||||
case Validation_Error:
|
||||
fmt.wprintfln(stderr, "[%T] %s", specific_error, specific_error.message)
|
||||
case Help_Request:
|
||||
write_usage(stdout, data_type, program, style)
|
||||
}
|
||||
}
|
||||
/*
|
||||
Get the value for a subtag.
|
||||
|
||||
This is useful if you need to parse through the `args` tag for a struct field
|
||||
on a custom type setter or custom flag checker.
|
||||
|
||||
Example:
|
||||
|
||||
import "core:flags"
|
||||
import "core:fmt"
|
||||
|
||||
subtag_example :: proc() {
|
||||
args_tag := "precision=3,signed"
|
||||
|
||||
precision, has_precision := flags.get_subtag(args_tag, "precision")
|
||||
signed, is_signed := flags.get_subtag(args_tag, "signed")
|
||||
|
||||
fmt.printfln("precision = %q, %t", precision, has_precision)
|
||||
fmt.printfln("signed = %q, %t", signed, is_signed)
|
||||
}
|
||||
|
||||
Output:
|
||||
|
||||
precision = "3", true
|
||||
signed = "", true
|
||||
|
||||
*/
|
||||
get_subtag :: proc(tag, id: string) -> (value: string, ok: bool) {
|
||||
// This proc was initially private in `internal_rtti.odin`, but given how
|
||||
// useful it would be to custom type setters and flag checkers, it lives
|
||||
// here now.
|
||||
|
||||
tag := tag
|
||||
|
||||
for subtag in strings.split_iterator(&tag, ",") {
|
||||
if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] {
|
||||
return subtag[1 + equals:], true
|
||||
} else if id == subtag {
|
||||
return "", true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package flags
|
||||
|
||||
/*
|
||||
Check a flag after parsing, during the validation stage.
|
||||
|
||||
Inputs:
|
||||
- model: A raw pointer to the data structure provided to `parse`.
|
||||
- name: The name of the flag being checked.
|
||||
- value: An `any` type that contains the value to be checked.
|
||||
- args_tag: The `args` tag from within the struct.
|
||||
|
||||
Returns:
|
||||
- error: An error message, or an empty string if no error occurred.
|
||||
*/
|
||||
Custom_Flag_Checker :: #type proc(
|
||||
model: rawptr,
|
||||
name: string,
|
||||
value: any,
|
||||
args_tag: string,
|
||||
) -> (
|
||||
error: string,
|
||||
)
|
||||
|
||||
@(private)
|
||||
global_custom_flag_checker: Custom_Flag_Checker
|
||||
|
||||
/*
|
||||
Set the global custom flag checker.
|
||||
|
||||
Note that only one can be active at a time.
|
||||
|
||||
Inputs:
|
||||
- checker: The flag checker. Pass `nil` to disable any previously set checker.
|
||||
*/
|
||||
register_flag_checker :: proc(checker: Custom_Flag_Checker) {
|
||||
global_custom_flag_checker = checker
|
||||
}
|
||||
+10
-4
@@ -1973,11 +1973,13 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
// fi.hash = false;
|
||||
fi.indent += 1
|
||||
|
||||
if !is_soa && hash {
|
||||
is_empty := len(info.names) == 0
|
||||
|
||||
if !is_soa && hash && !is_empty {
|
||||
io.write_byte(fi.writer, '\n', &fi.n)
|
||||
}
|
||||
defer {
|
||||
if hash {
|
||||
if !is_soa && hash && !is_empty {
|
||||
for _ in 0..<indent { io.write_byte(fi.writer, '\t', &fi.n) }
|
||||
}
|
||||
io.write_byte(fi.writer, ']' if is_soa && the_verb == 'v' else '}', &fi.n)
|
||||
@@ -2025,9 +2027,9 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
}
|
||||
io.write_string(fi.writer, base_type_name, &fi.n)
|
||||
io.write_byte(fi.writer, '{', &fi.n)
|
||||
if hash { io.write_byte(fi.writer, '\n', &fi.n) }
|
||||
if hash && !is_empty { io.write_byte(fi.writer, '\n', &fi.n) }
|
||||
defer {
|
||||
if hash {
|
||||
if hash && !is_empty {
|
||||
fi.indent -= 1
|
||||
fmt_write_indent(fi)
|
||||
fi.indent += 1
|
||||
@@ -2075,6 +2077,10 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
if hash { io.write_string(fi.writer, ",\n", &fi.n) }
|
||||
}
|
||||
}
|
||||
|
||||
if hash && n > 0 {
|
||||
for _ in 0..<indent { io.write_byte(fi.writer, '\t', &fi.n) }
|
||||
}
|
||||
} else {
|
||||
field_count := -1
|
||||
for name, i in info.names {
|
||||
|
||||
@@ -495,8 +495,10 @@ unique :: proc(s: $S/[]$T) -> S where intrinsics.type_is_comparable(T) #no_bound
|
||||
}
|
||||
i := 1
|
||||
for j in 1..<len(s) {
|
||||
if s[j] != s[j-1] && i != j {
|
||||
s[i] = s[j]
|
||||
if s[j] != s[j-1] {
|
||||
if i != j {
|
||||
s[i] = s[j]
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
@@ -513,8 +515,10 @@ unique_proc :: proc(s: $S/[]$T, eq: proc(T, T) -> bool) -> S #no_bounds_check {
|
||||
}
|
||||
i := 1
|
||||
for j in 1..<len(s) {
|
||||
if !eq(s[j], s[j-1]) && i != j {
|
||||
s[i] = s[j]
|
||||
if !eq(s[j], s[j-1]) {
|
||||
if i != j {
|
||||
s[i] = s[j]
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,20 @@ to_string :: proc(b: Builder) -> (res: string) {
|
||||
return string(b.buf[:])
|
||||
}
|
||||
/*
|
||||
Appends a trailing null byte after the end of the current Builder byte buffer and then casts it to a cstring
|
||||
|
||||
Inputs:
|
||||
- b: A pointer to builder
|
||||
|
||||
Returns:
|
||||
- res: A cstring of the Builder's buffer
|
||||
*/
|
||||
to_cstring :: proc(b: ^Builder) -> (res: cstring) {
|
||||
append(&b.buf, 0)
|
||||
pop(&b.buf)
|
||||
return cstring(raw_data(b.buf))
|
||||
}
|
||||
/*
|
||||
Returns the length of the Builder's buffer, in bytes
|
||||
|
||||
Inputs:
|
||||
|
||||
@@ -30,8 +30,8 @@ get_last_error :: proc "contextless" () -> int {
|
||||
}
|
||||
|
||||
_futex_wait :: proc "contextless" (futex: ^Futex, expected: u32) -> bool {
|
||||
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), 0, 0, 0) == -1 {
|
||||
switch get_last_error() {
|
||||
if error, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), 0, 0, 0); !ok {
|
||||
switch error {
|
||||
case EINTR, EAGAIN:
|
||||
return true
|
||||
case:
|
||||
@@ -45,11 +45,11 @@ _futex_wait_with_timeout :: proc "contextless" (futex: ^Futex, expected: u32, du
|
||||
if duration <= 0 {
|
||||
return false
|
||||
}
|
||||
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), cast(uintptr) &Time_Spec{
|
||||
if error, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAIT_PRIVATE, uintptr(expected), cast(uintptr) &Time_Spec{
|
||||
time_sec = cast(uint)(duration / 1e9),
|
||||
time_nsec = cast(uint)(duration % 1e9),
|
||||
}, 0, 0) == -1 {
|
||||
switch get_last_error() {
|
||||
}, 0, 0); !ok {
|
||||
switch error {
|
||||
case EINTR, EAGAIN:
|
||||
return true
|
||||
case ETIMEDOUT:
|
||||
@@ -62,13 +62,13 @@ _futex_wait_with_timeout :: proc "contextless" (futex: ^Futex, expected: u32, du
|
||||
}
|
||||
|
||||
_futex_signal :: proc "contextless" (futex: ^Futex) {
|
||||
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, 1, 0, 0, 0) == -1 {
|
||||
if _, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, 1, 0, 0, 0); !ok {
|
||||
_panic("futex_wake_single failure")
|
||||
}
|
||||
}
|
||||
|
||||
_futex_broadcast :: proc "contextless" (futex: ^Futex) {
|
||||
if cast(int) intrinsics.syscall(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, uintptr(max(i32)), 0, 0, 0) == -1 {
|
||||
if _, ok := intrinsics.syscall_bsd(unix.SYS___futex, uintptr(futex), FUTEX_WAKE_PRIVATE, uintptr(max(i32)), 0, 0, 0); !ok {
|
||||
_panic("_futex_wake_all failure")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,3 +712,7 @@ Window_setDelegate :: proc "c" (self: ^Window, delegate: ^WindowDelegate) {
|
||||
Window_backingScaleFactor :: proc "c" (self: ^Window) -> Float {
|
||||
return msgSend(Float, self, "backingScaleFactor")
|
||||
}
|
||||
@(objc_type=Window, objc_name="setWantsLayer")
|
||||
Window_setWantsLayer :: proc "c" (self: ^Window, ok: BOOL) {
|
||||
msgSend(nil, self, "setWantsLayer:", ok)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ version_string_buf: [1024]u8
|
||||
init_os_version :: proc () {
|
||||
os_version.platform = .FreeBSD
|
||||
|
||||
kernel_version_buf: [129]u8
|
||||
kernel_version_buf: [1024]u8
|
||||
|
||||
b := strings.builder_from_bytes(version_string_buf[:])
|
||||
// Retrieve kernel info using `sysctl`, e.g. FreeBSD 13.1-RELEASE-p2 GENERIC
|
||||
|
||||
@@ -5,14 +5,15 @@ import "base:intrinsics"
|
||||
|
||||
sysctl :: proc(mib: []i32, val: ^$T) -> (ok: bool) {
|
||||
mib := mib
|
||||
result_size := i64(size_of(T))
|
||||
result_size := u64(size_of(T))
|
||||
|
||||
res := intrinsics.syscall(SYS_sysctl,
|
||||
res: uintptr
|
||||
res, ok = intrinsics.syscall_bsd(SYS_sysctl,
|
||||
uintptr(raw_data(mib)), uintptr(len(mib)),
|
||||
uintptr(val), uintptr(&result_size),
|
||||
uintptr(0), uintptr(0),
|
||||
)
|
||||
return res == 0
|
||||
return
|
||||
}
|
||||
|
||||
// See /usr/include/sys/sysctl.h for details
|
||||
|
||||
@@ -38,16 +38,30 @@ foreign kernel32 {
|
||||
lpNumberOfCharsWritten: LPDWORD,
|
||||
lpReserved: LPVOID) -> BOOL ---
|
||||
|
||||
PeekConsoleInputW :: proc(hConsoleInput: HANDLE,
|
||||
lpBuffer: ^INPUT_RECORD,
|
||||
nLength: DWORD,
|
||||
lpNumberOfEventsRead: LPDWORD) -> BOOL ---
|
||||
|
||||
ReadConsoleInputW :: proc(hConsoleInput: HANDLE,
|
||||
lpBuffer: ^INPUT_RECORD,
|
||||
nLength: DWORD,
|
||||
lpNumberOfEventsRead: LPDWORD) -> BOOL ---
|
||||
|
||||
GetConsoleMode :: proc(hConsoleHandle: HANDLE,
|
||||
lpMode: LPDWORD) -> BOOL ---
|
||||
SetConsoleMode :: proc(hConsoleHandle: HANDLE,
|
||||
dwMode: DWORD) -> BOOL ---
|
||||
SetConsoleCursorPosition :: proc(hConsoleHandle: HANDLE,
|
||||
dwCursorPosition: COORD) -> BOOL ---
|
||||
dwCursorPosition: COORD) -> BOOL ---
|
||||
SetConsoleTextAttribute :: proc(hConsoleOutput: HANDLE,
|
||||
wAttributes: WORD) -> BOOL ---
|
||||
wAttributes: WORD) -> BOOL ---
|
||||
GetConsoleCP :: proc() -> UINT ---
|
||||
SetConsoleCP :: proc(wCodePageID: UINT) -> BOOL ---
|
||||
GetConsoleOutputCP :: proc() -> UINT ---
|
||||
SetConsoleOutputCP :: proc(wCodePageID: UINT) -> BOOL ---
|
||||
|
||||
FlushConsoleInputBuffer :: proc(hConsoleInput: HANDLE) -> BOOL ---
|
||||
|
||||
GetFileInformationByHandle :: proc(hFile: HANDLE, lpFileInformation: LPBY_HANDLE_FILE_INFORMATION) -> BOOL ---
|
||||
SetHandleInformation :: proc(hObject: HANDLE,
|
||||
dwMask: DWORD,
|
||||
@@ -85,6 +99,12 @@ foreign kernel32 {
|
||||
RemoveDirectoryW :: proc(lpPathName: LPCWSTR) -> BOOL ---
|
||||
SetFileAttributesW :: proc(lpFileName: LPCWSTR, dwFileAttributes: DWORD) -> BOOL ---
|
||||
SetLastError :: proc(dwErrCode: DWORD) ---
|
||||
GetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
|
||||
SetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
|
||||
ClearCommError :: proc(hFile: HANDLE, lpErrors: ^Com_Error, lpStat: ^COMSTAT) -> BOOL ---
|
||||
GetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
|
||||
SetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
|
||||
GetCommPorts :: proc(lpPortNumbers: PULONG, uPortNumbersCount: ULONG, puPortNumbersFound: PULONG) -> ULONG ---
|
||||
GetCommandLineW :: proc() -> LPCWSTR ---
|
||||
GetTempPathW :: proc(nBufferLength: DWORD, lpBuffer: LPCWSTR) -> DWORD ---
|
||||
GetCurrentProcess :: proc() -> HANDLE ---
|
||||
@@ -413,6 +433,7 @@ foreign kernel32 {
|
||||
LoadLibraryW :: proc(c_str: LPCWSTR) -> HMODULE ---
|
||||
FreeLibrary :: proc(h: HMODULE) -> BOOL ---
|
||||
GetProcAddress :: proc(h: HMODULE, c_str: LPCSTR) -> rawptr ---
|
||||
LoadLibraryExW :: proc(c_str: LPCWSTR, file: HANDLE, flags: LoadLibraryEx_Flags) -> HMODULE ---
|
||||
|
||||
|
||||
GetFullPathNameW :: proc(filename: LPCWSTR, buffer_length: DWORD, buffer: LPCWSTR, file_part: ^LPCWSTR) -> DWORD ---
|
||||
@@ -1015,82 +1036,6 @@ PHANDLER_ROUTINE :: HandlerRoutine
|
||||
// NOTE(Jeroen, 2024-06-13): As Odin now supports bit_fields, we no longer need
|
||||
// a helper procedure. `init_dcb_with_config` and `get_dcb_config` have been removed.
|
||||
|
||||
DTR_Control :: enum byte {
|
||||
Disable = 0,
|
||||
Enable = 1,
|
||||
Handshake = 2,
|
||||
}
|
||||
RTS_Control :: enum byte {
|
||||
Disable = 0,
|
||||
Enable = 1,
|
||||
Handshake = 2,
|
||||
Toggle = 3,
|
||||
}
|
||||
Parity :: enum byte {
|
||||
None = 0,
|
||||
Odd = 1,
|
||||
Even = 2,
|
||||
Mark = 3,
|
||||
Space = 4,
|
||||
}
|
||||
Stop_Bits :: enum byte {
|
||||
One = 0,
|
||||
One_And_A_Half = 1,
|
||||
Two = 2,
|
||||
}
|
||||
|
||||
DCB :: struct {
|
||||
DCBlength: DWORD,
|
||||
BaudRate: DWORD,
|
||||
using _: bit_field DWORD {
|
||||
fBinary: bool | 1,
|
||||
fParity: bool | 1,
|
||||
fOutxCtsFlow: bool | 1,
|
||||
fOutxDsrFlow: bool | 1,
|
||||
fDtrControl: DTR_Control | 2,
|
||||
fDsrSensitivity: bool | 1,
|
||||
fTXContinueOnXoff: bool | 1,
|
||||
fOutX: bool | 1,
|
||||
fInX: bool | 1,
|
||||
fErrorChar: bool | 1,
|
||||
fNull: bool | 1,
|
||||
fRtsControl: RTS_Control | 2,
|
||||
fAbortOnError: bool | 1,
|
||||
},
|
||||
wReserved: WORD,
|
||||
XOnLim: WORD,
|
||||
XOffLim: WORD,
|
||||
ByteSize: BYTE,
|
||||
Parity: Parity,
|
||||
StopBits: Stop_Bits,
|
||||
XonChar: byte,
|
||||
XoffChar: byte,
|
||||
ErrorChar: byte,
|
||||
EofChar: byte,
|
||||
EvtChar: byte,
|
||||
wReserved1: WORD,
|
||||
}
|
||||
|
||||
@(default_calling_convention="system")
|
||||
foreign kernel32 {
|
||||
GetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
|
||||
SetCommState :: proc(handle: HANDLE, dcb: ^DCB) -> BOOL ---
|
||||
}
|
||||
|
||||
COMMTIMEOUTS :: struct {
|
||||
ReadIntervalTimeout: DWORD,
|
||||
ReadTotalTimeoutMultiplier: DWORD,
|
||||
ReadTotalTimeoutConstant: DWORD,
|
||||
WriteTotalTimeoutMultiplier: DWORD,
|
||||
WriteTotalTimeoutConstant: DWORD,
|
||||
}
|
||||
|
||||
@(default_calling_convention="system")
|
||||
foreign kernel32 {
|
||||
GetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
|
||||
SetCommTimeouts :: proc(handle: HANDLE, timeouts: ^COMMTIMEOUTS) -> BOOL ---
|
||||
}
|
||||
|
||||
LPFIBER_START_ROUTINE :: #type proc "system" (lpFiberParameter: LPVOID)
|
||||
|
||||
@(default_calling_convention = "system")
|
||||
@@ -1167,9 +1112,9 @@ Battery_Flag :: enum BYTE {
|
||||
Low = 1,
|
||||
Critical = 2,
|
||||
Charging = 3,
|
||||
No_Battery = 7,
|
||||
No_Battery = 7,
|
||||
}
|
||||
Battery_Flags :: bit_set[Battery_Flag; BYTE]
|
||||
Battery_Flags :: bit_set[Battery_Flag; BYTE]
|
||||
|
||||
/* Global Memory Flags */
|
||||
GMEM_FIXED :: 0x0000
|
||||
|
||||
@@ -2674,6 +2674,22 @@ OSVERSIONINFOEXW :: struct {
|
||||
wReserved: UCHAR,
|
||||
}
|
||||
|
||||
LoadLibraryEx_Flag :: enum DWORD {
|
||||
LOAD_LIBRARY_AS_DATAFILE = 1, // 1 << 1: 0x0002,
|
||||
LOAD_WITH_ALTERED_SEARCH_PATH = 3, // 1 << 3: 0x0008,
|
||||
LOAD_IGNORE_CODE_AUTHZ_LEVEL = 4, // 1 << 4: 0x0010,
|
||||
LOAD_LIBRARY_AS_IMAGE_RESOURCE = 5, // 1 << 5: 0x0020,
|
||||
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 6, // 1 << 6: 0x0040,
|
||||
LOAD_LIBRARY_REQUIRE_SIGNED_TARGET = 7, // 1 << 7: 0x0080,
|
||||
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 8, // 1 << 8: 0x0100,
|
||||
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 9, // 1 << 9: 0x0200,
|
||||
LOAD_LIBRARY_SEARCH_USER_DIRS = 10, // 1 << 10: 0x0400,
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 11, // 1 << 11: 0x0800,
|
||||
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 12, // 1 << 12: 0x1000,
|
||||
LOAD_LIBRARY_SAFE_CURRENT_DIRS = 13, // 1 << 13: 0x2000,
|
||||
}
|
||||
LoadLibraryEx_Flags :: distinct bit_set[LoadLibraryEx_Flag]
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-quota_limits
|
||||
// Used in LogonUserExW
|
||||
PQUOTA_LIMITS :: struct {
|
||||
@@ -3983,6 +3999,70 @@ CONSOLE_CURSOR_INFO :: struct {
|
||||
PCONSOLE_SCREEN_BUFFER_INFO :: ^CONSOLE_SCREEN_BUFFER_INFO
|
||||
PCONSOLE_CURSOR_INFO :: ^CONSOLE_CURSOR_INFO
|
||||
|
||||
Event_Type :: enum WORD {
|
||||
KEY_EVENT = 0x0001,
|
||||
MOUSE_EVENT = 0x0002,
|
||||
WINDOW_BUFFER_SIZE_EVENT = 0x0004,
|
||||
MENU_EVENT = 0x0008,
|
||||
FOCUS_EVENT = 0x0010,
|
||||
}
|
||||
|
||||
INPUT_RECORD :: struct {
|
||||
EventType: Event_Type,
|
||||
Event: struct #raw_union {
|
||||
KeyEvent: KEY_EVENT_RECORD,
|
||||
MouseEvent: MOUSE_EVENT_RECORD,
|
||||
WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
|
||||
MenuEvent: MENU_EVENT_RECORD,
|
||||
FocusEvent: FOCUS_EVENT_RECORD,
|
||||
},
|
||||
}
|
||||
|
||||
Control_Key_State_Bits :: enum {
|
||||
RIGHT_ALT_PRESSED,
|
||||
LEFT_ALT_PRESSED,
|
||||
RIGHT_CTRL_PRESSED,
|
||||
LEFT_CTRL_PRESSED,
|
||||
SHIFT_PRESSED,
|
||||
NUMLOCK_ON,
|
||||
SCROLLLOCK_ON,
|
||||
CAPSLOCK_ON,
|
||||
ENHANCED_KEY,
|
||||
}
|
||||
Control_Key_State :: bit_set[Control_Key_State_Bits; DWORD]
|
||||
|
||||
KEY_EVENT_RECORD :: struct {
|
||||
bKeyDown: BOOL,
|
||||
wRepeatCount: WORD,
|
||||
wVirtualKeyCode: WORD,
|
||||
wVirtualScanCode: WORD,
|
||||
uChar: struct #raw_union {
|
||||
UnicodeChar: WCHAR,
|
||||
AsciiChar: CHAR,
|
||||
},
|
||||
dwControlKeyState: Control_Key_State,
|
||||
}
|
||||
|
||||
MOUSE_EVENT_RECORD :: struct {
|
||||
dwMousePosition: COORD,
|
||||
dwButtonState: DWORD,
|
||||
dwControlKeyState: DWORD,
|
||||
dwEventFlags: DWORD,
|
||||
}
|
||||
|
||||
WINDOW_BUFFER_SIZE_RECORD :: struct {
|
||||
dwSize: COORD,
|
||||
}
|
||||
|
||||
MENU_EVENT_RECORD :: struct {
|
||||
dwCommandId: UINT,
|
||||
}
|
||||
|
||||
FOCUS_EVENT_RECORD :: struct {
|
||||
bSetFocus: BOOL,
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Networking
|
||||
//
|
||||
@@ -4217,3 +4297,92 @@ SOCKADDR :: struct {
|
||||
sa_family: ADDRESS_FAMILY,
|
||||
sa_data: [14]CHAR,
|
||||
}
|
||||
|
||||
DTR_Control :: enum byte {
|
||||
Disable = 0,
|
||||
Enable = 1,
|
||||
Handshake = 2,
|
||||
}
|
||||
RTS_Control :: enum byte {
|
||||
Disable = 0,
|
||||
Enable = 1,
|
||||
Handshake = 2,
|
||||
Toggle = 3,
|
||||
}
|
||||
Parity :: enum byte {
|
||||
None = 0,
|
||||
Odd = 1,
|
||||
Even = 2,
|
||||
Mark = 3,
|
||||
Space = 4,
|
||||
}
|
||||
Stop_Bits :: enum byte {
|
||||
One = 0,
|
||||
One_And_A_Half = 1,
|
||||
Two = 2,
|
||||
}
|
||||
|
||||
DCB :: struct {
|
||||
DCBlength: DWORD,
|
||||
BaudRate: DWORD,
|
||||
using _: bit_field DWORD {
|
||||
fBinary: bool | 1,
|
||||
fParity: bool | 1,
|
||||
fOutxCtsFlow: bool | 1,
|
||||
fOutxDsrFlow: bool | 1,
|
||||
fDtrControl: DTR_Control | 2,
|
||||
fDsrSensitivity: bool | 1,
|
||||
fTXContinueOnXoff: bool | 1,
|
||||
fOutX: bool | 1,
|
||||
fInX: bool | 1,
|
||||
fErrorChar: bool | 1,
|
||||
fNull: bool | 1,
|
||||
fRtsControl: RTS_Control | 2,
|
||||
fAbortOnError: bool | 1,
|
||||
},
|
||||
wReserved: WORD,
|
||||
XOnLim: WORD,
|
||||
XOffLim: WORD,
|
||||
ByteSize: BYTE,
|
||||
Parity: Parity,
|
||||
StopBits: Stop_Bits,
|
||||
XonChar: byte,
|
||||
XoffChar: byte,
|
||||
ErrorChar: byte,
|
||||
EofChar: byte,
|
||||
EvtChar: byte,
|
||||
wReserved1: WORD,
|
||||
}
|
||||
|
||||
COMMTIMEOUTS :: struct {
|
||||
ReadIntervalTimeout: DWORD,
|
||||
ReadTotalTimeoutMultiplier: DWORD,
|
||||
ReadTotalTimeoutConstant: DWORD,
|
||||
WriteTotalTimeoutMultiplier: DWORD,
|
||||
WriteTotalTimeoutConstant: DWORD,
|
||||
}
|
||||
|
||||
Com_Stat_Bits :: enum {
|
||||
fCtsHold,
|
||||
fDsrHold,
|
||||
fRlsdHold,
|
||||
fXoffHold,
|
||||
fXoffSent,
|
||||
fEof,
|
||||
fTxim,
|
||||
}
|
||||
COMSTAT :: struct {
|
||||
bits: bit_set[Com_Stat_Bits; DWORD],
|
||||
cbInQue: DWORD,
|
||||
cbOutQue: DWORD,
|
||||
}
|
||||
|
||||
Com_Error_Bits :: enum {
|
||||
RXOVER,
|
||||
OVERRUN,
|
||||
RXPARITY,
|
||||
FRAME,
|
||||
BREAK,
|
||||
}
|
||||
Com_Error :: bit_set[Com_Error_Bits; DWORD]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import "core:encoding/ansi"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
@require import pkg_log "core:log"
|
||||
import "core:math/rand"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
@@ -106,6 +107,13 @@ run_test_task :: proc(task: thread.Task) {
|
||||
options = Default_Test_Logger_Opts,
|
||||
}
|
||||
|
||||
random_generator_state: runtime.Default_Random_State
|
||||
context.random_generator = {
|
||||
procedure = runtime.default_random_generator_proc,
|
||||
data = &random_generator_state,
|
||||
}
|
||||
rand.reset(data.t.seed)
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
|
||||
data.it.p(&data.t)
|
||||
|
||||
+40
-38
@@ -1,31 +1,44 @@
|
||||
|
||||
/*
|
||||
The `i18n` package is flexible and easy to use.
|
||||
The `i18n` package is a flexible and easy to use way to localise applications.
|
||||
|
||||
It has one call to get a translation: `get`, which the user can alias into something like `T`.
|
||||
It has two calls to get a translation: `get()` and `get_n()`, which the user can alias into something like `T` and `Tn`
|
||||
with statements like:
|
||||
T :: i18n.get
|
||||
Tn :: i18n.get_n.
|
||||
|
||||
`get`, referred to as `T` here, has a few different signatures.
|
||||
All of them will return the key if the entry can't be found in the active translation catalog.
|
||||
`get()` is used for retrieving the translation of sentences which **never** change in form,
|
||||
like for instance "Connection established" or "All temporary files have been deleted".
|
||||
Note that the number (singular, dual, plural, whatever else) is not relevant: the sentence is fixed and it will have only one possible translation in any other language.
|
||||
|
||||
- `T(key)` returns the translation of `key`.
|
||||
- `T(key, n)` returns a pluralized translation of `key` according to value `n`.
|
||||
`get_n()` is used for retrieving the translations of sentences which change according to the number of items referenced.
|
||||
The various signatures of `get_n()` have one more parameter, `n`, which will receive that number and be used
|
||||
to select the correct form according to the pluralizer attached to the message catalogue when initially loaded;
|
||||
for instance, to summarise a rather complex matter, some languages use the singular form when referring to 0 items and some use the (only in their case) plural forms;
|
||||
also, languages may have more or less quantifier forms than a single singular form and a universal plural form:
|
||||
for instance, Chinese has just one form for any quantity, while Welsh may have up to 6 different forms for specific different quantities.
|
||||
|
||||
- `T(section, key)` returns the translation of `key` in `section`.
|
||||
- `T(section, key, n)` returns a pluralized translation of `key` in `section` according to value `n`.
|
||||
Both `get()` and `get_n()`, referred to as `T` and `Tn` here, have several different signatures.
|
||||
All of them will return the key if the entry can't be found in the active translation catalogue.
|
||||
By default lookup take place in the global `i18n.ACTIVE` catalogue for ease of use, unless a specific catalogue is supplied.
|
||||
|
||||
By default lookup take place in the global `i18n.ACTIVE` catalog for ease of use.
|
||||
If you want to override which translation to use, for example in a language preview dialog, you can use the following:
|
||||
- `T(key)` returns the translation of `key`.
|
||||
- `T(key, catalog)` returns the translation of `key` from explictly supplied catalogue.
|
||||
- `T(section, key)` returns the translation of `key` in `section`.
|
||||
- `T(section, key, catalog)` returns the translation of `key` in `section` from explictly supplied catalogue.
|
||||
|
||||
- `T(key, n, catalog)` returns the pluralized version of `key` from explictly supplied catalog.
|
||||
- `T(section, key, n, catalog)` returns the pluralized version of `key` in `section` from explictly supplied catalog.
|
||||
- `Tn(key, n)` returns the translation of `key` according to number of items `n`.
|
||||
- `Tn(key, n, catalog)` returns the translation of `key` from explictly supplied catalogue.
|
||||
- `Tn(section, key, n)` returns the translation of `key` in `section` according to number of items `n`.
|
||||
- `Tn(section, key, n, catalog)` returns the translation of `key` in `section` according to number of items `n` from explictly supplied catalogue.
|
||||
|
||||
If a catalog has translation contexts or sections, then omitting it in the above calls looks up in section "".
|
||||
|
||||
The default pluralization rule is n != 1, which is to say that passing n == 1 (or not passing n) returns the singular form.
|
||||
Passing n != 1 returns plural form 1.
|
||||
The default pluralization rule is `n != 1`, which is to say that passing `n == 1` returns the singular form (in slot 0).
|
||||
Passing `n != 1` returns the plural form in slot 1 (if any).
|
||||
|
||||
Should a language not conform to this rule, you can pass a pluralizer procedure to the catalog parser.
|
||||
This is a procedure that maps an integer to an integer, taking a value and returning which plural slot should be used.
|
||||
This is a procedure that maps an integer to an integer, taking a quantity and returning which plural slot should be used.
|
||||
|
||||
You can also assign it to a loaded catalog after parsing, of course.
|
||||
|
||||
@@ -34,24 +47,21 @@ Example:
|
||||
import "core:fmt"
|
||||
import "core:text/i18n"
|
||||
|
||||
T :: i18n.get
|
||||
T :: i18n.get
|
||||
Tn :: i18n.get_n
|
||||
|
||||
mo :: proc() {
|
||||
using fmt
|
||||
|
||||
err: i18n.Error
|
||||
|
||||
/*
|
||||
Parse MO file and set it as the active translation so we can omit `get`'s "catalog" parameter.
|
||||
*/
|
||||
// Parse MO file and set it as the active translation so we can omit `get`'s "catalog" parameter.
|
||||
i18n.ACTIVE, err = i18n.parse_mo(#load("translations/nl_NL.mo"))
|
||||
defer i18n.destroy()
|
||||
|
||||
if err != .None { return }
|
||||
|
||||
/*
|
||||
These are in the .MO catalog.
|
||||
*/
|
||||
// These are in the .MO catalog.
|
||||
println("-----")
|
||||
println(T(""))
|
||||
println("-----")
|
||||
@@ -60,13 +70,11 @@ Example:
|
||||
println(T("Hellope, World!"))
|
||||
println("-----")
|
||||
// We pass 1 into `T` to get the singular format string, then 1 again into printf.
|
||||
printf(T("There is %d leaf.\n", 1), 1)
|
||||
printf(Tn("There is %d leaf.\n", 1), 1)
|
||||
// We pass 42 into `T` to get the plural format string, then 42 again into printf.
|
||||
printf(T("There is %d leaf.\n", 42), 42)
|
||||
printf(Tn("There is %d leaf.\n", 42), 42)
|
||||
|
||||
/*
|
||||
This isn't in the translation catalog, so the key is passed back untranslated.
|
||||
*/
|
||||
// This isn't in the translation catalog, so the key is passed back untranslated.
|
||||
println("-----")
|
||||
println(T("Come visit us on Discord!"))
|
||||
}
|
||||
@@ -76,19 +84,13 @@ Example:
|
||||
|
||||
err: i18n.Error
|
||||
|
||||
/*
|
||||
Parse QT file and set it as the active translation so we can omit `get`'s "catalog" parameter.
|
||||
*/
|
||||
// Parse QT file and set it as the active translation so we can omit `get`'s "catalog" parameter.
|
||||
i18n.ACTIVE, err = i18n.parse_qt(#load("translations/nl_NL-qt-ts.ts"))
|
||||
defer i18n.destroy()
|
||||
|
||||
if err != .None {
|
||||
return
|
||||
}
|
||||
if err != .None { return }
|
||||
|
||||
/*
|
||||
These are in the .TS catalog. As you can see they have sections.
|
||||
*/
|
||||
// These are in the .TS catalog. As you can see they have sections.
|
||||
println("--- Page section ---")
|
||||
println("Page:Text for translation =", T("Page", "Text for translation"))
|
||||
println("-----")
|
||||
@@ -99,8 +101,8 @@ Example:
|
||||
println("-----")
|
||||
println("--- apple_count section ---")
|
||||
println("apple_count:%d apple(s) =")
|
||||
println("\t 1 =", T("apple_count", "%d apple(s)", 1))
|
||||
println("\t 42 =", T("apple_count", "%d apple(s)", 42))
|
||||
println("\t 1 =", Tn("apple_count", "%d apple(s)", 1))
|
||||
println("\t 42 =", Tn("apple_count", "%d apple(s)", 42))
|
||||
}
|
||||
*/
|
||||
package i18n
|
||||
|
||||
+118
-70
@@ -10,23 +10,13 @@ package i18n
|
||||
*/
|
||||
import "core:strings"
|
||||
|
||||
/*
|
||||
TODO:
|
||||
- Support for more translation catalog file formats.
|
||||
*/
|
||||
|
||||
/*
|
||||
Currently active catalog.
|
||||
*/
|
||||
// Currently active catalog.
|
||||
ACTIVE: ^Translation
|
||||
|
||||
// Allow between 1 and 255 plural forms. Default: 10.
|
||||
MAX_PLURALS :: min(max(#config(ODIN_i18N_MAX_PLURAL_FORMS, 10), 1), 255)
|
||||
|
||||
/*
|
||||
The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
|
||||
*/
|
||||
|
||||
// The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
|
||||
Section :: map[string][]string
|
||||
|
||||
Translation :: struct {
|
||||
@@ -37,34 +27,24 @@ Translation :: struct {
|
||||
}
|
||||
|
||||
Error :: enum {
|
||||
/*
|
||||
General return values.
|
||||
*/
|
||||
// General return values.
|
||||
None = 0,
|
||||
Empty_Translation_Catalog,
|
||||
Duplicate_Key,
|
||||
|
||||
/*
|
||||
Couldn't find, open or read file.
|
||||
*/
|
||||
// Couldn't find, open or read file.
|
||||
File_Error,
|
||||
|
||||
/*
|
||||
File too short.
|
||||
*/
|
||||
// File too short.
|
||||
Premature_EOF,
|
||||
|
||||
/*
|
||||
GNU Gettext *.MO file errors.
|
||||
*/
|
||||
// GNU Gettext *.MO file errors.
|
||||
MO_File_Invalid_Signature,
|
||||
MO_File_Unsupported_Version,
|
||||
MO_File_Invalid,
|
||||
MO_File_Incorrect_Plural_Count,
|
||||
|
||||
/*
|
||||
Qt Linguist *.TS file errors.
|
||||
*/
|
||||
// Qt Linguist *.TS file errors.
|
||||
TS_File_Parse_Error,
|
||||
TS_File_Expected_Context,
|
||||
TS_File_Expected_Context_Name,
|
||||
@@ -85,73 +65,142 @@ DEFAULT_PARSE_OPTIONS :: Parse_Options{
|
||||
}
|
||||
|
||||
/*
|
||||
Several ways to use:
|
||||
- get(key), which defaults to the singular form and i18n.ACTIVE catalog, or
|
||||
- get(key, number), which returns the appropriate plural from the active catalog, or
|
||||
- get(key, number, catalog) to grab text from a specific one.
|
||||
*/
|
||||
get_single_section :: proc(key: string, number := 1, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
/*
|
||||
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
|
||||
*/
|
||||
plural := 1 if number != 1 else 0
|
||||
Returns the first translation string for the passed `key`.
|
||||
It is also aliased with `get()`.
|
||||
|
||||
if catalog.pluralize != nil {
|
||||
plural = catalog.pluralize(number)
|
||||
}
|
||||
return get_by_slot(key, plural, catalog)
|
||||
Two ways to use it:
|
||||
- get(key), which defaults to the `i18n.ACTIVE` catalogue, or
|
||||
- get(key, catalog) to grab text from a specific loaded catalogue
|
||||
|
||||
Inputs:
|
||||
- key: the string to translate
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string, or the original `key` if no translation was found.
|
||||
*/
|
||||
get_single_section :: proc(key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
return get_by_slot(key, 0, catalog)
|
||||
}
|
||||
|
||||
/*
|
||||
Several ways to use:
|
||||
- get(section, key), which defaults to the singular form and i18n.ACTIVE catalog, or
|
||||
- get(section, key, number), which returns the appropriate plural from the active catalog, or
|
||||
- get(section, key, number, catalog) to grab text from a specific one.
|
||||
*/
|
||||
get_by_section :: proc(section, key: string, number := 1, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
/*
|
||||
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
|
||||
*/
|
||||
plural := 1 if number != 1 else 0
|
||||
Returns the first translation string for the passed `key` in a specific section or context.
|
||||
It is also aliases with `get()`.
|
||||
|
||||
if catalog.pluralize != nil {
|
||||
plural = catalog.pluralize(number)
|
||||
}
|
||||
return get_by_slot(section, key, plural, catalog)
|
||||
Two ways to use it:
|
||||
- get(section, key), which defaults to the `i18n.ACTIVE` catalogue, or
|
||||
- get(section, key, catalog) to grab text from a specific loaded catalogue
|
||||
|
||||
Inputs:
|
||||
- section: the catalogue section (sometimes also called 'context') in which to look up the translation
|
||||
- key: the string to translate
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string, or the original `key` if no translation was found.
|
||||
*/
|
||||
get_by_section :: proc(section, key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
return get_by_slot(section, key, 0, catalog)
|
||||
}
|
||||
|
||||
get :: proc{get_single_section, get_by_section}
|
||||
|
||||
/*
|
||||
Several ways to use:
|
||||
- get_by_slot(key), which defaults to the singular form and i18n.ACTIVE catalog, or
|
||||
- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
|
||||
- get_by_slot(key, slot, catalog) to grab text from a specific one.
|
||||
Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue).
|
||||
It is also aliased with `get_n()`.
|
||||
|
||||
Two ways to use it:
|
||||
- get_n(key, quantity), which returns the appropriate plural from the active catalogue, or
|
||||
- get_n(key, quantity, catalog) to grab text from a specific loaded catalogue
|
||||
|
||||
Inputs:
|
||||
- key: the string to translate
|
||||
- quantity: the quantity of item to be used to select the correct plural form
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string, or the original `key` if no translation was found.
|
||||
*/
|
||||
get_single_section_with_quantity :: proc(key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
/*
|
||||
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
|
||||
*/
|
||||
slot := 1 if quantity != 1 else 0
|
||||
|
||||
if catalog.pluralize != nil {
|
||||
slot = catalog.pluralize(quantity)
|
||||
}
|
||||
return get_by_slot(key, slot, catalog)
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue)
|
||||
in a specific section or context.
|
||||
It is also aliases with `get_n()`.
|
||||
|
||||
Two ways to use it:
|
||||
- get(section, key, quantity), which returns the appropriate plural from the active catalogue, or
|
||||
- get(section, key, quantity, catalog) to grab text from a specific loaded catalogue
|
||||
|
||||
Inputs:
|
||||
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
|
||||
- key: the string to translate
|
||||
- qantity: the quantity of item to be used to select the correct plural form
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string, or the original `key` if no translation was found
|
||||
*/
|
||||
get_by_section_with_quantity :: proc(section, key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
/*
|
||||
A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
|
||||
*/
|
||||
slot := 1 if quantity != 1 else 0
|
||||
|
||||
if catalog.pluralize != nil {
|
||||
slot = catalog.pluralize(quantity)
|
||||
}
|
||||
return get_by_slot(section, key, slot, catalog)
|
||||
}
|
||||
get_n :: proc{get_single_section_with_quantity, get_by_section_with_quantity}
|
||||
|
||||
/*
|
||||
Two ways to use:
|
||||
- get_by_slot(key, slot), which returns the requested plural from the active catalogue, or
|
||||
- get_by_slot(key, slot, catalog) to grab text from a specific loaded catalogue.
|
||||
|
||||
If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
|
||||
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
|
||||
|
||||
Inputs:
|
||||
- key: the string to translate.
|
||||
- slot: the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string, or the original `key` if no translation was found.
|
||||
*/
|
||||
get_by_slot_single_section :: proc(key: string, slot := 0, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
get_by_slot_single_section :: proc(key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
return get_by_slot_by_section("", key, slot, catalog)
|
||||
}
|
||||
|
||||
/*
|
||||
Several ways to use:
|
||||
- get_by_slot(key), which defaults to the singular form and i18n.ACTIVE catalog, or
|
||||
Two ways to use:
|
||||
- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
|
||||
- get_by_slot(key, slot, catalog) to grab text from a specific one.
|
||||
|
||||
If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
|
||||
|
||||
Inputs:
|
||||
- section: the catalogue section (sometime also called 'context') from which to lookup the translation
|
||||
- key: the string to translate.
|
||||
- slot: the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
|
||||
- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)
|
||||
|
||||
Returns: the translated string or the original `key` if no translation was found.
|
||||
*/
|
||||
get_by_slot_by_section :: proc(section, key: string, slot := 0, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
get_by_slot_by_section :: proc(section, key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
|
||||
if catalog == nil || section not_in catalog.k_v {
|
||||
/*
|
||||
Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
|
||||
*/
|
||||
// Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
|
||||
return key
|
||||
}
|
||||
|
||||
/*
|
||||
Return the translation from the requested slot if this key is known, else return the key.
|
||||
*/
|
||||
// Return the translation from the requested slot if this key is known, else return the key.
|
||||
if translations, ok := catalog.k_v[section][key]; ok {
|
||||
plural := min(max(0, slot), len(catalog.k_v[section][key]) - 1)
|
||||
return translations[plural]
|
||||
@@ -161,7 +210,6 @@ get_by_slot_by_section :: proc(section, key: string, slot := 0, catalog: ^Transl
|
||||
get_by_slot :: proc{get_by_slot_single_section, get_by_slot_by_section}
|
||||
|
||||
/*
|
||||
Same for destroy:
|
||||
- destroy(), to clean up the currently active catalog catalog i18n.ACTIVE
|
||||
- destroy(catalog), to clean up a specific catalog.
|
||||
*/
|
||||
|
||||
+301
-4
@@ -5,6 +5,12 @@ REPLACEMENT_CHAR :: '\ufffd' // Represented an invalid code point
|
||||
MAX_ASCII :: '\u007f' // Maximum ASCII value
|
||||
MAX_LATIN1 :: '\u00ff' // Maximum Latin-1 value
|
||||
|
||||
ZERO_WIDTH_SPACE :: '\u200B'
|
||||
ZERO_WIDTH_NON_JOINER :: '\u200C'
|
||||
ZERO_WIDTH_JOINER :: '\u200D'
|
||||
WORD_JOINER :: '\u2060'
|
||||
|
||||
@(require_results)
|
||||
binary_search :: proc(c: i32, table: []i32, length, stride: int) -> int {
|
||||
n := length
|
||||
t := 0
|
||||
@@ -24,6 +30,7 @@ binary_search :: proc(c: i32, table: []i32, length, stride: int) -> int {
|
||||
return -1
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
to_lower :: proc(r: rune) -> rune {
|
||||
c := i32(r)
|
||||
p := binary_search(c, to_lower_ranges[:], len(to_lower_ranges)/3, 3)
|
||||
@@ -36,6 +43,7 @@ to_lower :: proc(r: rune) -> rune {
|
||||
}
|
||||
return rune(c)
|
||||
}
|
||||
@(require_results)
|
||||
to_upper :: proc(r: rune) -> rune {
|
||||
c := i32(r)
|
||||
p := binary_search(c, to_upper_ranges[:], len(to_upper_ranges)/3, 3)
|
||||
@@ -48,6 +56,7 @@ to_upper :: proc(r: rune) -> rune {
|
||||
}
|
||||
return rune(c)
|
||||
}
|
||||
@(require_results)
|
||||
to_title :: proc(r: rune) -> rune {
|
||||
c := i32(r)
|
||||
p := binary_search(c, to_upper_singlets[:], len(to_title_singlets)/2, 2)
|
||||
@@ -58,6 +67,7 @@ to_title :: proc(r: rune) -> rune {
|
||||
}
|
||||
|
||||
|
||||
@(require_results)
|
||||
is_lower :: proc(r: rune) -> bool {
|
||||
if r <= MAX_ASCII {
|
||||
return u32(r)-'a' < 26
|
||||
@@ -74,6 +84,7 @@ is_lower :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_upper :: proc(r: rune) -> bool {
|
||||
if r <= MAX_ASCII {
|
||||
return u32(r)-'A' < 26
|
||||
@@ -91,6 +102,7 @@ is_upper :: proc(r: rune) -> bool {
|
||||
}
|
||||
|
||||
is_alpha :: is_letter
|
||||
@(require_results)
|
||||
is_letter :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pLmask != 0
|
||||
@@ -111,10 +123,12 @@ is_letter :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_title :: proc(r: rune) -> bool {
|
||||
return is_upper(r) && is_lower(r)
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_digit :: proc(r: rune) -> bool {
|
||||
if r <= MAX_LATIN1 {
|
||||
return '0' <= r && r <= '9'
|
||||
@@ -124,6 +138,7 @@ is_digit :: proc(r: rune) -> bool {
|
||||
|
||||
|
||||
is_white_space :: is_space
|
||||
@(require_results)
|
||||
is_space :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
switch r {
|
||||
@@ -140,18 +155,20 @@ is_space :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_combining :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
|
||||
return c >= 0x0300 && (c <= 0x036f ||
|
||||
(c >= 0x1ab0 && c <= 0x1aff) ||
|
||||
(c >= 0x1dc0 && c <= 0x1dff) ||
|
||||
(c >= 0x20d0 && c <= 0x20ff) ||
|
||||
(c >= 0xfe20 && c <= 0xfe2f))
|
||||
(c >= 0x1ab0 && c <= 0x1aff) ||
|
||||
(c >= 0x1dc0 && c <= 0x1dff) ||
|
||||
(c >= 0x20d0 && c <= 0x20ff) ||
|
||||
(c >= 0xfe20 && c <= 0xfe2f))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@(require_results)
|
||||
is_graphic :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pg != 0
|
||||
@@ -159,6 +176,7 @@ is_graphic :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_print :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pp != 0
|
||||
@@ -166,6 +184,7 @@ is_print :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_control :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pC != 0
|
||||
@@ -173,6 +192,7 @@ is_control :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_number :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pN != 0
|
||||
@@ -180,6 +200,7 @@ is_number :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_punct :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pP != 0
|
||||
@@ -187,9 +208,285 @@ is_punct :: proc(r: rune) -> bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
is_symbol :: proc(r: rune) -> bool {
|
||||
if u32(r) <= MAX_LATIN1 {
|
||||
return char_properties[u8(r)]&pS != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// The procedures below are accurate as of Unicode 15.1.0.
|
||||
//
|
||||
|
||||
// Emoji_Modifier
|
||||
@(require_results)
|
||||
is_emoji_modifier :: proc(r: rune) -> bool {
|
||||
return 0x1F3FB <= r && r <= 0x1F3FF
|
||||
}
|
||||
|
||||
// Regional_Indicator
|
||||
@(require_results)
|
||||
is_regional_indicator :: proc(r: rune) -> bool {
|
||||
return 0x1F1E6 <= r && r <= 0x1F1FF
|
||||
}
|
||||
|
||||
// General_Category=Enclosing_Mark
|
||||
@(require_results)
|
||||
is_enclosing_mark :: proc(r: rune) -> bool {
|
||||
switch r {
|
||||
case 0x0488,
|
||||
0x0489,
|
||||
0x1ABE,
|
||||
0x20DD ..= 0x20E0,
|
||||
0x20E2 ..= 0x20E4,
|
||||
0xA670 ..= 0xA672:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Prepended_Concatenation_Mark
|
||||
@(require_results)
|
||||
is_prepended_concatenation_mark :: proc(r: rune) -> bool {
|
||||
switch r {
|
||||
case 0x00600 ..= 0x00605,
|
||||
0x006DD,
|
||||
0x0070F,
|
||||
0x00890 ..= 0x00891,
|
||||
0x008E2,
|
||||
0x110BD,
|
||||
0x110CD:
|
||||
return true
|
||||
case:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// General_Category=Spacing_Mark
|
||||
@(require_results)
|
||||
is_spacing_mark :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, spacing_mark_ranges[:], len(spacing_mark_ranges)/2, 2)
|
||||
if p >= 0 && spacing_mark_ranges[p] <= c && c <= spacing_mark_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// General_Category=Nonspacing_Mark
|
||||
@(require_results)
|
||||
is_nonspacing_mark :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, nonspacing_mark_ranges[:], len(nonspacing_mark_ranges)/2, 2)
|
||||
if p >= 0 && nonspacing_mark_ranges[p] <= c && c <= nonspacing_mark_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Extended_Pictographic
|
||||
@(require_results)
|
||||
is_emoji_extended_pictographic :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, emoji_extended_pictographic_ranges[:], len(emoji_extended_pictographic_ranges)/2, 2)
|
||||
if p >= 0 && emoji_extended_pictographic_ranges[p] <= c && c <= emoji_extended_pictographic_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Grapheme_Extend
|
||||
@(require_results)
|
||||
is_grapheme_extend :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, grapheme_extend_ranges[:], len(grapheme_extend_ranges)/2, 2)
|
||||
if p >= 0 && grapheme_extend_ranges[p] <= c && c <= grapheme_extend_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Hangul_Syllable_Type=Leading_Jamo
|
||||
@(require_results)
|
||||
is_hangul_syllable_leading :: proc(r: rune) -> bool {
|
||||
return 0x1100 <= r && r <= 0x115F || 0xA960 <= r && r <= 0xA97C
|
||||
}
|
||||
|
||||
// Hangul_Syllable_Type=Vowel_Jamo
|
||||
@(require_results)
|
||||
is_hangul_syllable_vowel :: proc(r: rune) -> bool {
|
||||
return 0x1160 <= r && r <= 0x11A7 || 0xD7B0 <= r && r <= 0xD7C6
|
||||
}
|
||||
|
||||
// Hangul_Syllable_Type=Trailing_Jamo
|
||||
@(require_results)
|
||||
is_hangul_syllable_trailing :: proc(r: rune) -> bool {
|
||||
return 0x11A8 <= r && r <= 0x11FF || 0xD7CB <= r && r <= 0xD7FB
|
||||
}
|
||||
|
||||
// Hangul_Syllable_Type=LV_Syllable
|
||||
@(require_results)
|
||||
is_hangul_syllable_lv :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, hangul_syllable_lv_singlets[:], len(hangul_syllable_lv_singlets), 1)
|
||||
if p >= 0 && c == hangul_syllable_lv_singlets[p] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Hangul_Syllable_Type=LVT_Syllable
|
||||
@(require_results)
|
||||
is_hangul_syllable_lvt :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, hangul_syllable_lvt_ranges[:], len(hangul_syllable_lvt_ranges)/2, 2)
|
||||
if p >= 0 && hangul_syllable_lvt_ranges[p] <= c && c <= hangul_syllable_lvt_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Indic_Syllabic_Category=Consonant_Preceding_Repha
|
||||
@(require_results)
|
||||
is_indic_consonant_preceding_repha :: proc(r: rune) -> bool {
|
||||
switch r {
|
||||
case 0x00D4E,
|
||||
0x11941,
|
||||
0x11D46,
|
||||
0x11F02:
|
||||
return true
|
||||
case:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Indic_Syllabic_Category=Consonant_Prefixed
|
||||
@(require_results)
|
||||
is_indic_consonant_prefixed :: proc(r: rune) -> bool {
|
||||
switch r {
|
||||
case 0x111C2 ..= 0x111C3,
|
||||
0x1193F,
|
||||
0x11A3A,
|
||||
0x11A84 ..= 0x11A89:
|
||||
return true
|
||||
case:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Indic_Conjunct_Break=Linker
|
||||
@(require_results)
|
||||
is_indic_conjunct_break_linker :: proc(r: rune) -> bool {
|
||||
switch r {
|
||||
case 0x094D,
|
||||
0x09CD,
|
||||
0x0ACD,
|
||||
0x0B4D,
|
||||
0x0C4D,
|
||||
0x0D4D:
|
||||
return true
|
||||
case:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Indic_Conjunct_Break=Consonant
|
||||
@(require_results)
|
||||
is_indic_conjunct_break_consonant :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, indic_conjunct_break_consonant_ranges[:], len(indic_conjunct_break_consonant_ranges)/2, 2)
|
||||
if p >= 0 && indic_conjunct_break_consonant_ranges[p] <= c && c <= indic_conjunct_break_consonant_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Indic_Conjunct_Break=Extend
|
||||
@(require_results)
|
||||
is_indic_conjunct_break_extend :: proc(r: rune) -> bool {
|
||||
c := i32(r)
|
||||
p := binary_search(c, indic_conjunct_break_extend_ranges[:], len(indic_conjunct_break_extend_ranges)/2, 2)
|
||||
if p >= 0 && indic_conjunct_break_extend_ranges[p] <= c && c <= indic_conjunct_break_extend_ranges[p+1] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
For grapheme text segmentation, from Unicode TR 29 Rev 43:
|
||||
|
||||
```
|
||||
Indic_Syllabic_Category = Consonant_Preceding_Repha, or
|
||||
Indic_Syllabic_Category = Consonant_Prefixed, or
|
||||
Prepended_Concatenation_Mark = Yes
|
||||
```
|
||||
*/
|
||||
@(require_results)
|
||||
is_gcb_prepend_class :: proc(r: rune) -> bool {
|
||||
return is_indic_consonant_preceding_repha(r) || is_indic_consonant_prefixed(r) || is_prepended_concatenation_mark(r)
|
||||
}
|
||||
|
||||
/*
|
||||
For grapheme text segmentation, from Unicode TR 29 Rev 43:
|
||||
|
||||
```
|
||||
Grapheme_Extend = Yes, or
|
||||
Emoji_Modifier = Yes
|
||||
|
||||
This includes:
|
||||
General_Category = Nonspacing_Mark
|
||||
General_Category = Enclosing_Mark
|
||||
U+200C ZERO WIDTH NON-JOINER
|
||||
|
||||
plus a few General_Category = Spacing_Mark needed for canonical equivalence.
|
||||
```
|
||||
*/
|
||||
@(require_results)
|
||||
is_gcb_extend_class :: proc(r: rune) -> bool {
|
||||
return is_grapheme_extend(r) || is_emoji_modifier(r)
|
||||
}
|
||||
|
||||
// Return values:
|
||||
//
|
||||
// - 2 if East_Asian_Width=F or W, or
|
||||
// - 0 if non-printable / zero-width, or
|
||||
// - 1 in all other cases.
|
||||
//
|
||||
@(require_results)
|
||||
normalized_east_asian_width :: proc(r: rune) -> int {
|
||||
// This is a different interpretation of the BOM which occurs in the middle of text.
|
||||
ZERO_WIDTH_NO_BREAK_SPACE :: '\uFEFF'
|
||||
|
||||
if is_control(r) {
|
||||
return 0
|
||||
} else if r <= 0x10FF {
|
||||
// Easy early out for low runes.
|
||||
return 1
|
||||
}
|
||||
|
||||
switch r {
|
||||
case ZERO_WIDTH_NO_BREAK_SPACE,
|
||||
ZERO_WIDTH_SPACE,
|
||||
ZERO_WIDTH_NON_JOINER,
|
||||
ZERO_WIDTH_JOINER,
|
||||
WORD_JOINER:
|
||||
return 0
|
||||
}
|
||||
|
||||
c := i32(r)
|
||||
p := binary_search(c, normalized_east_asian_width_ranges[:], len(normalized_east_asian_width_ranges)/3, 3)
|
||||
if p >= 0 && normalized_east_asian_width_ranges[p] <= c && c <= normalized_east_asian_width_ranges[p+1] {
|
||||
return cast(int)normalized_east_asian_width_ranges[p+2]
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
//
|
||||
// End of Unicode 15.1.0 block.
|
||||
//
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
package utf8
|
||||
|
||||
import "core:unicode"
|
||||
|
||||
ZERO_WIDTH_JOINER :: unicode.ZERO_WIDTH_JOINER
|
||||
is_control :: unicode.is_control
|
||||
is_hangul_syllable_leading :: unicode.is_hangul_syllable_leading
|
||||
is_hangul_syllable_vowel :: unicode.is_hangul_syllable_vowel
|
||||
is_hangul_syllable_trailing :: unicode.is_hangul_syllable_trailing
|
||||
is_hangul_syllable_lv :: unicode.is_hangul_syllable_lv
|
||||
is_hangul_syllable_lvt :: unicode.is_hangul_syllable_lvt
|
||||
is_indic_conjunct_break_extend :: unicode.is_indic_conjunct_break_extend
|
||||
is_indic_conjunct_break_linker :: unicode.is_indic_conjunct_break_linker
|
||||
is_indic_conjunct_break_consonant :: unicode.is_indic_conjunct_break_consonant
|
||||
is_gcb_extend_class :: unicode.is_gcb_extend_class
|
||||
is_spacing_mark :: unicode.is_spacing_mark
|
||||
is_gcb_prepend_class :: unicode.is_gcb_prepend_class
|
||||
is_emoji_extended_pictographic :: unicode.is_emoji_extended_pictographic
|
||||
is_regional_indicator :: unicode.is_regional_indicator
|
||||
normalized_east_asian_width :: unicode.normalized_east_asian_width
|
||||
|
||||
|
||||
Grapheme :: struct {
|
||||
byte_index: int,
|
||||
rune_index: int,
|
||||
width: int,
|
||||
}
|
||||
|
||||
/*
|
||||
Count the individual graphemes in a UTF-8 string.
|
||||
|
||||
Inputs:
|
||||
- str: The input string.
|
||||
|
||||
Returns:
|
||||
- graphemes: The number of graphemes in the string.
|
||||
- runes: The number of runes in the string.
|
||||
- width: The width of the string in number of monospace cells.
|
||||
*/
|
||||
@(require_results)
|
||||
grapheme_count :: proc(str: string) -> (graphemes, runes, width: int) {
|
||||
_, graphemes, runes, width = decode_grapheme_clusters(str, false)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Decode the individual graphemes in a UTF-8 string.
|
||||
|
||||
*Allocates Using Provided Allocator*
|
||||
|
||||
Inputs:
|
||||
- str: The input string.
|
||||
- track_graphemes: Whether or not to allocate and return `graphemes` with extra data about each grapheme.
|
||||
- allocator: (default: context.allocator)
|
||||
|
||||
Returns:
|
||||
- graphemes: Extra data about each grapheme.
|
||||
- grapheme_count: The number of graphemes in the string.
|
||||
- rune_count: The number of runes in the string.
|
||||
- width: The width of the string in number of monospace cells.
|
||||
*/
|
||||
@(require_results)
|
||||
decode_grapheme_clusters :: proc(
|
||||
str: string,
|
||||
track_graphemes := true,
|
||||
allocator := context.allocator,
|
||||
) -> (
|
||||
graphemes: [dynamic]Grapheme,
|
||||
grapheme_count: int,
|
||||
rune_count: int,
|
||||
width: int,
|
||||
) {
|
||||
// The following procedure implements text segmentation by breaking on
|
||||
// Grapheme Cluster Boundaries[1], using the values[2] and rules[3] from
|
||||
// the Unicode® Standard Annex #29, entitled:
|
||||
//
|
||||
// UNICODE TEXT SEGMENTATION
|
||||
//
|
||||
// Version: Unicode 15.1.0
|
||||
// Date: 2023-08-16
|
||||
// Revision: 43
|
||||
//
|
||||
// This procedure is conformant[4] to UAX29-C1-1, otherwise known as the
|
||||
// extended, non-legacy ruleset.
|
||||
//
|
||||
// Please see the references below for more information.
|
||||
//
|
||||
//
|
||||
// NOTE(Feoramund): This procedure has not been highly optimized.
|
||||
// A couple opportunities were taken to bypass repeated checking when a
|
||||
// rune is outside of certain codepoint ranges, but little else has been
|
||||
// done. Standard switches, conditionals, and binary search are used to
|
||||
// see if a rune fits into a certain category.
|
||||
//
|
||||
// I did find that only one prior rune of state was necessary to build an
|
||||
// algorithm that successfully passes all 4,835 test cases provided with
|
||||
// this implementation from the Unicode organization's website.
|
||||
//
|
||||
// My initial implementation tracked explicit breaks and counted them once
|
||||
// the string iteration had terminated. I've found this current
|
||||
// implementation to be far simpler and need no allocations (unless the
|
||||
// caller wants position data).
|
||||
//
|
||||
// Most rules work backwards instead of forwards which has helped keep this
|
||||
// simple, despite its length and verbosity.
|
||||
//
|
||||
//
|
||||
// The implementation has been left verbose and in the order described by
|
||||
// the specification, to enable better readability and future upkeep.
|
||||
//
|
||||
// Some possible optimizations might include:
|
||||
//
|
||||
// - saving the type of `last_rune` instead of the exact rune.
|
||||
// - reordering rules.
|
||||
// - combining tables.
|
||||
//
|
||||
//
|
||||
// [1]: https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
|
||||
// [2]: https://www.unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table
|
||||
// [3]: https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
|
||||
// [4]: https://www.unicode.org/reports/tr29/#Conformance
|
||||
|
||||
// Additionally, this procedure now takes into account Standard Annex #11,
|
||||
// in order to estimate how visually wide the string will appear on a
|
||||
// monospaced display. This can only ever be a rough guess, as this tends
|
||||
// to be an implementation detail relating to which fonts are being used,
|
||||
// how codepoints are interpreted and drawn, if codepoint sequences are
|
||||
// interpreted correctly, and et cetera.
|
||||
//
|
||||
// For example, a program may not properly interpret an emoji modifier
|
||||
// sequence and print the component glyphs instead of one whole glyph.
|
||||
//
|
||||
// See here for more information: https://www.unicode.org/reports/tr11/
|
||||
//
|
||||
// NOTE: There is no explicit mention of what to do with zero-width spaces
|
||||
// as far as grapheme cluster segmentation goes, therefore this
|
||||
// implementation may count and return graphemes with a `width` of zero.
|
||||
//
|
||||
// Treat them as any other space.
|
||||
|
||||
Grapheme_Cluster_Sequence :: enum {
|
||||
None,
|
||||
Indic,
|
||||
Emoji,
|
||||
Regional,
|
||||
}
|
||||
|
||||
context.allocator = allocator
|
||||
|
||||
last_rune: rune
|
||||
last_rune_breaks_forward: bool
|
||||
|
||||
last_width: int
|
||||
last_grapheme_count: int
|
||||
|
||||
bypass_next_rune: bool
|
||||
|
||||
regional_indicator_counter: int
|
||||
|
||||
current_sequence: Grapheme_Cluster_Sequence
|
||||
continue_sequence: bool
|
||||
|
||||
for this_rune, byte_index in str {
|
||||
defer {
|
||||
// "Break at the start and end of text, unless the text is empty."
|
||||
//
|
||||
// GB1: sot ÷ Any
|
||||
// GB2: Any ÷ eot
|
||||
if rune_count == 0 && grapheme_count == 0 {
|
||||
grapheme_count += 1
|
||||
}
|
||||
|
||||
if grapheme_count > last_grapheme_count {
|
||||
width += normalized_east_asian_width(this_rune)
|
||||
if track_graphemes {
|
||||
append(&graphemes, Grapheme{
|
||||
byte_index,
|
||||
rune_count,
|
||||
width - last_width,
|
||||
})
|
||||
}
|
||||
last_grapheme_count = grapheme_count
|
||||
last_width = width
|
||||
}
|
||||
|
||||
last_rune = this_rune
|
||||
rune_count += 1
|
||||
|
||||
if !continue_sequence {
|
||||
current_sequence = .None
|
||||
regional_indicator_counter = 0
|
||||
}
|
||||
continue_sequence = false
|
||||
}
|
||||
|
||||
// "Do not break between a CR and LF. Otherwise, break before and after controls."
|
||||
//
|
||||
// GB3: CR × LF
|
||||
// GB4: (Control | CR | LF) ÷
|
||||
// GB5: ÷ (Control | CR | LF)
|
||||
if this_rune == '\n' && last_rune == '\r' {
|
||||
last_rune_breaks_forward = false
|
||||
bypass_next_rune = false
|
||||
continue
|
||||
}
|
||||
|
||||
if is_control(this_rune) {
|
||||
grapheme_count += 1
|
||||
last_rune_breaks_forward = true
|
||||
bypass_next_rune = true
|
||||
continue
|
||||
}
|
||||
|
||||
// (This check is for rules that work forwards, instead of backwards.)
|
||||
if bypass_next_rune {
|
||||
if last_rune_breaks_forward {
|
||||
grapheme_count += 1
|
||||
last_rune_breaks_forward = false
|
||||
}
|
||||
|
||||
bypass_next_rune = false
|
||||
continue
|
||||
}
|
||||
|
||||
// (Optimization 1: Prevent low runes from proceeding further.)
|
||||
//
|
||||
// * 0xA9 and 0xAE are in the Extended_Pictographic range,
|
||||
// which is checked later in GB11.
|
||||
if this_rune != 0xA9 && this_rune != 0xAE && this_rune <= 0x2FF {
|
||||
grapheme_count += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// (Optimization 2: Check if the rune is in the Hangul space before getting specific.)
|
||||
if 0x1100 <= this_rune && this_rune <= 0xD7FB {
|
||||
// "Do not break Hangul syllable sequences."
|
||||
//
|
||||
// GB6: L × (L | V | LV | LVT)
|
||||
// GB7: (LV | V) × (V | T)
|
||||
// GB8: (LVT | T) × T
|
||||
if is_hangul_syllable_leading(this_rune) ||
|
||||
is_hangul_syllable_lv(this_rune) ||
|
||||
is_hangul_syllable_lvt(this_rune)
|
||||
{
|
||||
if !is_hangul_syllable_leading(last_rune) {
|
||||
grapheme_count += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if is_hangul_syllable_vowel(this_rune) {
|
||||
if is_hangul_syllable_leading(last_rune) ||
|
||||
is_hangul_syllable_vowel(last_rune) ||
|
||||
is_hangul_syllable_lv(last_rune)
|
||||
{
|
||||
continue
|
||||
}
|
||||
grapheme_count += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if is_hangul_syllable_trailing(this_rune) {
|
||||
if is_hangul_syllable_trailing(last_rune) ||
|
||||
is_hangul_syllable_lvt(last_rune) ||
|
||||
is_hangul_syllable_lv(last_rune) ||
|
||||
is_hangul_syllable_vowel(last_rune)
|
||||
{
|
||||
continue
|
||||
}
|
||||
grapheme_count += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// "Do not break before extending characters or ZWJ."
|
||||
//
|
||||
// GB9: × (Extend | ZWJ)
|
||||
if this_rune == ZERO_WIDTH_JOINER {
|
||||
continue_sequence = true
|
||||
continue
|
||||
}
|
||||
|
||||
if is_gcb_extend_class(this_rune) {
|
||||
// (Support for GB9c.)
|
||||
if current_sequence == .Indic {
|
||||
if is_indic_conjunct_break_extend(this_rune) && (
|
||||
is_indic_conjunct_break_linker(last_rune) ||
|
||||
is_indic_conjunct_break_consonant(last_rune) )
|
||||
{
|
||||
continue_sequence = true
|
||||
continue
|
||||
}
|
||||
|
||||
if is_indic_conjunct_break_linker(this_rune) && (
|
||||
is_indic_conjunct_break_linker(last_rune) ||
|
||||
is_indic_conjunct_break_extend(last_rune) ||
|
||||
is_indic_conjunct_break_consonant(last_rune) )
|
||||
{
|
||||
continue_sequence = true
|
||||
continue
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// (Support for GB11.)
|
||||
if current_sequence == .Emoji && (
|
||||
is_gcb_extend_class(last_rune) ||
|
||||
is_emoji_extended_pictographic(last_rune) )
|
||||
{
|
||||
continue_sequence = true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// _The GB9a and GB9b rules only apply to extended grapheme clusters:_
|
||||
// "Do not break before SpacingMarks, or after Prepend characters."
|
||||
//
|
||||
// GB9a: × SpacingMark
|
||||
// GB9b: Prepend ×
|
||||
if is_spacing_mark(this_rune) {
|
||||
continue
|
||||
}
|
||||
|
||||
if is_gcb_prepend_class(this_rune) {
|
||||
grapheme_count += 1
|
||||
bypass_next_rune = true
|
||||
continue
|
||||
}
|
||||
|
||||
// _The GB9c rule only applies to extended grapheme clusters:_
|
||||
// "Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker."
|
||||
//
|
||||
// GB9c: \p{InCB=Consonant} [ \p{InCB=Extend} \p{InCB=Linker} ]* \p{InCB=Linker} [ \p{InCB=Extend} \p{InCB=Linker} ]* × \p{InCB=Consonant}
|
||||
if is_indic_conjunct_break_consonant(this_rune) {
|
||||
if current_sequence == .Indic {
|
||||
if last_rune == ZERO_WIDTH_JOINER ||
|
||||
is_indic_conjunct_break_linker(last_rune)
|
||||
{
|
||||
continue_sequence = true
|
||||
} else {
|
||||
grapheme_count += 1
|
||||
}
|
||||
} else {
|
||||
grapheme_count += 1
|
||||
current_sequence = .Indic
|
||||
continue_sequence = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if is_indic_conjunct_break_extend(this_rune) {
|
||||
if current_sequence == .Indic {
|
||||
if is_indic_conjunct_break_consonant(last_rune) ||
|
||||
is_indic_conjunct_break_linker(last_rune)
|
||||
{
|
||||
continue_sequence = true
|
||||
} else {
|
||||
grapheme_count += 1
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if is_indic_conjunct_break_linker(this_rune) {
|
||||
if current_sequence == .Indic {
|
||||
if is_indic_conjunct_break_extend(last_rune) ||
|
||||
is_indic_conjunct_break_linker(last_rune)
|
||||
{
|
||||
continue_sequence = true
|
||||
} else {
|
||||
grapheme_count += 1
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
//
|
||||
// (Curiously, there is no GB10.)
|
||||
//
|
||||
|
||||
// "Do not break within emoji modifier sequences or emoji zwj sequences."
|
||||
//
|
||||
// GB11: \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic}
|
||||
if is_emoji_extended_pictographic(this_rune) {
|
||||
if current_sequence != .Emoji || last_rune != ZERO_WIDTH_JOINER {
|
||||
grapheme_count += 1
|
||||
}
|
||||
current_sequence = .Emoji
|
||||
continue_sequence = true
|
||||
continue
|
||||
}
|
||||
|
||||
// "Do not break within emoji flag sequences.
|
||||
// That is, do not break between regional indicator (RI) symbols
|
||||
// if there is an odd number of RI characters before the break point."
|
||||
//
|
||||
// GB12: sot (RI RI)* RI × RI
|
||||
// GB13: [^RI] (RI RI)* RI × RI
|
||||
if is_regional_indicator(this_rune) {
|
||||
if regional_indicator_counter & 1 == 0 {
|
||||
grapheme_count += 1
|
||||
}
|
||||
|
||||
current_sequence = .Regional
|
||||
continue_sequence = true
|
||||
regional_indicator_counter += 1
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// "Otherwise, break everywhere."
|
||||
//
|
||||
// GB999: Any ÷ Any
|
||||
grapheme_count += 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user