mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-03 05:08:14 +00:00
Merge remote-tracking branch 'offical/master'
This commit is contained in:
@@ -5,13 +5,10 @@ The implementation is non-intrusive, and non-recursive.
|
||||
*/
|
||||
package container_avl
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
@(require) import "base:intrinsics"
|
||||
@(require) import "base:runtime"
|
||||
import "core:slice"
|
||||
|
||||
_ :: intrinsics
|
||||
_ :: runtime
|
||||
|
||||
// Originally based on the CC0 implementation by Eric Biggers
|
||||
// See: https://github.com/ebiggers/avl_tree/
|
||||
|
||||
@@ -675,4 +672,4 @@ iterator_first :: proc "contextless" (it: ^Iterator($Value)) {
|
||||
if it._cur != nil {
|
||||
it._next = node_next_or_prev_in_order(it._cur, it._direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
// This package implements a red-black tree
|
||||
package container_rbtree
|
||||
|
||||
@(require) import "base:intrinsics"
|
||||
@(require) import "base:runtime"
|
||||
import "core:slice"
|
||||
|
||||
// Originally based on the CC0 implementation from literateprograms.org
|
||||
// But with API design mimicking `core:container/avl` for ease of use.
|
||||
|
||||
// Direction specifies the traversal direction for a tree iterator.
|
||||
Direction :: enum i8 {
|
||||
// Backward is the in-order backwards direction.
|
||||
Backward = -1,
|
||||
// Forward is the in-order forwards direction.
|
||||
Forward = 1,
|
||||
}
|
||||
|
||||
Ordering :: slice.Ordering
|
||||
|
||||
// Tree is a red-black tree
|
||||
Tree :: struct($Key: typeid, $Value: typeid) {
|
||||
// user_data is a parameter that will be passed to the on_remove
|
||||
// callback.
|
||||
user_data: rawptr,
|
||||
// on_remove is an optional callback that can be called immediately
|
||||
// after a node is removed from the tree.
|
||||
on_remove: proc(key: Key, value: Value, user_data: rawptr),
|
||||
|
||||
_root: ^Node(Key, Value),
|
||||
_node_allocator: runtime.Allocator,
|
||||
_cmp_fn: proc(Key, Key) -> Ordering,
|
||||
_size: int,
|
||||
}
|
||||
|
||||
// Node is a red-black tree node.
|
||||
//
|
||||
// WARNING: It is unsafe to mutate value if the node is part of a tree
|
||||
// if doing so will alter the Node's sort position relative to other
|
||||
// elements in the tree.
|
||||
Node :: struct($Key: typeid, $Value: typeid) {
|
||||
key: Key,
|
||||
value: Value,
|
||||
|
||||
_parent: ^Node(Key, Value),
|
||||
_left: ^Node(Key, Value),
|
||||
_right: ^Node(Key, Value),
|
||||
_color: Color,
|
||||
}
|
||||
|
||||
// Might store this in the node pointer in the future, but that'll require a decent amount of rework to pass ^^N instead of ^N
|
||||
Color :: enum uintptr {Black = 0, Red = 1}
|
||||
|
||||
// Iterator is a tree iterator.
|
||||
//
|
||||
// WARNING: It is unsafe to modify the tree while iterating, except via
|
||||
// the iterator_remove method.
|
||||
Iterator :: struct($Key: typeid, $Value: typeid) {
|
||||
_tree: ^Tree(Key, Value),
|
||||
_cur: ^Node(Key, Value),
|
||||
_next: ^Node(Key, Value),
|
||||
_direction: Direction,
|
||||
_called_next: bool,
|
||||
}
|
||||
|
||||
// init initializes a tree.
|
||||
init :: proc {
|
||||
init_ordered,
|
||||
init_cmp,
|
||||
}
|
||||
|
||||
// init_cmp initializes a tree.
|
||||
init_cmp :: proc(t: ^$T/Tree($Key, $Value), cmp_fn: proc(a, b: Key) -> Ordering, node_allocator := context.allocator) {
|
||||
t._root = nil
|
||||
t._node_allocator = node_allocator
|
||||
t._cmp_fn = cmp_fn
|
||||
t._size = 0
|
||||
}
|
||||
|
||||
// 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_cmp(t, slice.cmp_proc(Key), node_allocator)
|
||||
}
|
||||
|
||||
// destroy de-initializes a tree.
|
||||
destroy :: proc(t: ^$T/Tree($Key, $Value), call_on_remove: bool = true) {
|
||||
iter := iterator(t, .Forward)
|
||||
for _ in iterator_next(&iter) {
|
||||
iterator_remove(&iter, call_on_remove)
|
||||
}
|
||||
}
|
||||
|
||||
len :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> (node_count: int) {
|
||||
return t._size
|
||||
}
|
||||
|
||||
// first returns the first node in the tree (in-order) or nil iff
|
||||
// the tree is empty.
|
||||
first :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> ^Node(Key, Value) {
|
||||
return tree_first_or_last_in_order(t, Direction.Backward)
|
||||
}
|
||||
|
||||
// last returns the last element in the tree (in-order) or nil iff
|
||||
// the tree is empty.
|
||||
last :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> ^Node(Key, Value) {
|
||||
return tree_first_or_last_in_order(t, Direction.Forward)
|
||||
}
|
||||
|
||||
// find finds the key in the tree, and returns the corresponding node, or nil iff the value is not present.
|
||||
find :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (node: ^Node(Key, Value)) {
|
||||
node = t._root
|
||||
for node != nil {
|
||||
switch t._cmp_fn(key, node.key) {
|
||||
case .Equal: return node
|
||||
case .Less: node = node._left
|
||||
case .Greater: node = node._right
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// find_value finds the key in the tree, and returns the corresponding value, or nil iff the value is not present.
|
||||
find_value :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (value: Value, ok: bool) #optional_ok {
|
||||
if n := find(t, key); n != nil {
|
||||
return n.value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// find_or_insert attempts to insert the value into the tree, and returns
|
||||
// the node, a boolean indicating if the value was inserted, and the
|
||||
// node allocator error if relevant. If the value is already present, the existing node is updated.
|
||||
find_or_insert :: proc(t: ^$T/Tree($Key, $Value), key: Key, value: Value) -> (n: ^Node(Key, Value), inserted: bool, err: runtime.Allocator_Error) {
|
||||
n_ptr := &t._root
|
||||
for n_ptr^ != nil {
|
||||
n = n_ptr^
|
||||
switch t._cmp_fn(key, n.key) {
|
||||
case .Less:
|
||||
n_ptr = &n._left
|
||||
case .Greater:
|
||||
n_ptr = &n._right
|
||||
case .Equal:
|
||||
return
|
||||
}
|
||||
}
|
||||
_parent := n
|
||||
|
||||
n = new_clone(Node(Key, Value){key=key, value=value, _parent=_parent, _color=.Red}, t._node_allocator) or_return
|
||||
n_ptr^ = n
|
||||
insert_case1(t, n)
|
||||
t._size += 1
|
||||
return n, true, nil
|
||||
}
|
||||
|
||||
// remove removes a node or value from the tree, and returns true iff the
|
||||
// removal was successful. While the node's value will be left intact,
|
||||
// the node itself will be freed via the tree's node allocator.
|
||||
remove :: proc {
|
||||
remove_key,
|
||||
remove_node,
|
||||
}
|
||||
|
||||
// remove_value removes a value from the tree, and returns true iff the
|
||||
// removal was successful. While the node's key + value will be left intact,
|
||||
// the node itself will be freed via the tree's node allocator.
|
||||
remove_key :: proc(t: ^$T/Tree($Key, $Value), key: Key, call_on_remove := true) -> bool {
|
||||
n := find(t, key)
|
||||
if n == nil {
|
||||
return false // Key not found, nothing to do
|
||||
}
|
||||
return remove_node(t, n, call_on_remove)
|
||||
}
|
||||
|
||||
// remove_node removes a node from the tree, and returns true iff the
|
||||
// removal was successful. While the node's key + value will be left intact,
|
||||
// the node itself will be freed via the tree's node allocator.
|
||||
remove_node :: proc(t: ^$T/Tree($Key, $Value), node: ^$N/Node(Key, Value), call_on_remove := true) -> (found: bool) {
|
||||
if node._parent == node || (node._parent == nil && t._root != node) {
|
||||
return false // Don't touch self-parented or dangling nodes.
|
||||
}
|
||||
node := node
|
||||
if node._left != nil && node._right != nil {
|
||||
// Copy key + value from predecessor and delete it instead
|
||||
predecessor := maximum_node(node._left)
|
||||
node.key = predecessor.key
|
||||
node.value = predecessor.value
|
||||
node = predecessor
|
||||
}
|
||||
|
||||
child := node._right == nil ? node._left : node._right
|
||||
if node_color(node) == .Black {
|
||||
node._color = node_color(child)
|
||||
remove_case1(t, node)
|
||||
}
|
||||
replace_node(t, node, child)
|
||||
if node._parent == nil && child != nil {
|
||||
child._color = .Black // root should be black
|
||||
}
|
||||
|
||||
if call_on_remove && t.on_remove != nil {
|
||||
t.on_remove(node.key, node.value, t.user_data)
|
||||
}
|
||||
free(node, t._node_allocator)
|
||||
t._size -= 1
|
||||
return true
|
||||
}
|
||||
|
||||
// iterator returns a tree iterator in the specified direction.
|
||||
iterator :: proc "contextless" (t: ^$T/Tree($Key, $Value), direction: Direction) -> Iterator(Key, Value) {
|
||||
it: Iterator(Key, Value)
|
||||
it._tree = cast(^Tree(Key, Value))t
|
||||
it._direction = direction
|
||||
|
||||
iterator_first(&it)
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
// iterator_from_pos returns a tree iterator in the specified direction,
|
||||
// spanning the range [pos, last] (inclusive).
|
||||
iterator_from_pos :: proc "contextless" (t: ^$T/Tree($Key, $Value), pos: ^Node(Key, Value), direction: Direction) -> Iterator(Key, Value) {
|
||||
it: Iterator(Key, Value)
|
||||
it._tree = transmute(^Tree(Key, Value))t
|
||||
it._direction = direction
|
||||
it._next = nil
|
||||
it._called_next = false
|
||||
|
||||
if it._cur = pos; pos != nil {
|
||||
it._next = node_next_or_prev_in_order(it._cur, it._direction)
|
||||
}
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
// iterator_get returns the node currently pointed to by the iterator,
|
||||
// or nil iff the node has been removed, the tree is empty, or the end
|
||||
// of the tree has been reached.
|
||||
iterator_get :: proc "contextless" (it: ^$I/Iterator($Key, $Value)) -> ^Node(Key, Value) {
|
||||
return it._cur
|
||||
}
|
||||
|
||||
// iterator_remove removes the node currently pointed to by the iterator,
|
||||
// and returns true iff the removal was successful. Semantics are the
|
||||
// same as the Tree remove.
|
||||
iterator_remove :: proc(it: ^$I/Iterator($Key, $Value), call_on_remove: bool = true) -> bool {
|
||||
if it._cur == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ok := remove_node(it._tree, it._cur , call_on_remove)
|
||||
if ok {
|
||||
it._cur = nil
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// iterator_next advances the iterator and returns the (node, true) or
|
||||
// or (nil, false) iff the end of the tree has been reached.
|
||||
//
|
||||
// Note: The first call to iterator_next will return the first node instead
|
||||
// of advancing the iterator.
|
||||
iterator_next :: proc "contextless" (it: ^$I/Iterator($Key, $Value)) -> (^Node(Key, Value), bool) {
|
||||
// This check is needed so that the first element gets returned from
|
||||
// a brand-new iterator, and so that the somewhat contrived case where
|
||||
// iterator_remove is called before the first call to iterator_next
|
||||
// returns the correct value.
|
||||
if !it._called_next {
|
||||
it._called_next = true
|
||||
|
||||
// There can be the contrived case where iterator_remove is
|
||||
// called before ever calling iterator_next, which needs to be
|
||||
// handled as an actual call to next.
|
||||
//
|
||||
// If this happens it._cur will be nil, so only return the
|
||||
// first value, if it._cur is valid.
|
||||
if it._cur != nil {
|
||||
return it._cur, true
|
||||
}
|
||||
}
|
||||
|
||||
if it._next == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
it._cur = it._next
|
||||
it._next = node_next_or_prev_in_order(it._cur, it._direction)
|
||||
|
||||
return it._cur, true
|
||||
}
|
||||
|
||||
@(private)
|
||||
tree_first_or_last_in_order :: proc "contextless" (t: ^$T/Tree($Key, $Value), direction: Direction) -> ^Node(Key, Value) {
|
||||
first, sign := t._root, i8(direction)
|
||||
if first != nil {
|
||||
for {
|
||||
tmp := node_get_child(first, sign)
|
||||
if tmp == nil {
|
||||
break
|
||||
}
|
||||
first = tmp
|
||||
}
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
@(private)
|
||||
node_get_child :: #force_inline proc "contextless" (n: ^Node($Key, $Value), sign: i8) -> ^Node(Key, Value) {
|
||||
if sign < 0 {
|
||||
return n._left
|
||||
}
|
||||
return n._right
|
||||
}
|
||||
|
||||
@(private)
|
||||
node_next_or_prev_in_order :: proc "contextless" (n: ^Node($Key, $Value), direction: Direction) -> ^Node(Key, Value) {
|
||||
next, tmp: ^Node(Key, Value)
|
||||
sign := i8(direction)
|
||||
|
||||
if next = node_get_child(n, +sign); next != nil {
|
||||
for {
|
||||
tmp = node_get_child(next, -sign)
|
||||
if tmp == nil {
|
||||
break
|
||||
}
|
||||
next = tmp
|
||||
}
|
||||
} else {
|
||||
tmp, next = n, n._parent
|
||||
for next != nil && tmp == node_get_child(next, +sign) {
|
||||
tmp, next = next, next._parent
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
@(private)
|
||||
iterator_first :: proc "contextless" (it: ^Iterator($Key, $Value)) {
|
||||
// This is private because behavior when the user manually calls
|
||||
// iterator_first followed by iterator_next is unintuitive, since
|
||||
// the first call to iterator_next MUST return the first node
|
||||
// instead of advancing so that `for node in iterator_next(&next)`
|
||||
// works as expected.
|
||||
|
||||
switch it._direction {
|
||||
case .Forward:
|
||||
it._cur = tree_first_or_last_in_order(it._tree, .Backward)
|
||||
case .Backward:
|
||||
it._cur = tree_first_or_last_in_order(it._tree, .Forward)
|
||||
}
|
||||
|
||||
it._next = nil
|
||||
it._called_next = false
|
||||
|
||||
if it._cur != nil {
|
||||
it._next = node_next_or_prev_in_order(it._cur, it._direction)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
grand_parent :: proc(n: ^$N/Node($Key, $Value)) -> (g: ^N) {
|
||||
return n._parent._parent
|
||||
}
|
||||
|
||||
@(private)
|
||||
sibling :: proc(n: ^$N/Node($Key, $Value)) -> (s: ^N) {
|
||||
if n == n._parent._left {
|
||||
return n._parent._right
|
||||
} else {
|
||||
return n._parent._left
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
uncle :: proc(n: ^$N/Node($Key, $Value)) -> (u: ^N) {
|
||||
return sibling(n._parent)
|
||||
}
|
||||
|
||||
@(private)
|
||||
rotate__left :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
r := n._right
|
||||
replace_node(t, n, r)
|
||||
n._right = r._left
|
||||
if r._left != nil {
|
||||
r._left._parent = n
|
||||
}
|
||||
r._left = n
|
||||
n._parent = r
|
||||
}
|
||||
|
||||
@(private)
|
||||
rotate__right :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
l := n._left
|
||||
replace_node(t, n, l)
|
||||
n._left = l._right
|
||||
if l._right != nil {
|
||||
l._right._parent = n
|
||||
}
|
||||
l._right = n
|
||||
n._parent = l
|
||||
}
|
||||
|
||||
@(private)
|
||||
replace_node :: proc(t: ^$T/Tree($Key, $Value), old_n: ^$N/Node(Key, Value), new_n: ^N) {
|
||||
if old_n._parent == nil {
|
||||
t._root = new_n
|
||||
} else {
|
||||
if (old_n == old_n._parent._left) {
|
||||
old_n._parent._left = new_n
|
||||
} else {
|
||||
old_n._parent._right = new_n
|
||||
}
|
||||
}
|
||||
if new_n != nil {
|
||||
new_n._parent = old_n._parent
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
insert_case1 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if n._parent == nil {
|
||||
n._color = .Black
|
||||
} else {
|
||||
insert_case2(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
insert_case2 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if node_color(n._parent) == .Black {
|
||||
return // Tree is still valid
|
||||
} else {
|
||||
insert_case3(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
insert_case3 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if node_color(uncle(n)) == .Red {
|
||||
n._parent._color = .Black
|
||||
uncle(n)._color = .Black
|
||||
grand_parent(n)._color = .Red
|
||||
insert_case1(t, grand_parent(n))
|
||||
} else {
|
||||
insert_case4(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
insert_case4 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
n := n
|
||||
if n == n._parent._right && n._parent == grand_parent(n)._left {
|
||||
rotate__left(t, n._parent)
|
||||
n = n._left
|
||||
} else if n == n._parent._left && n._parent == grand_parent(n)._right {
|
||||
rotate__right(t, n._parent)
|
||||
n = n._right
|
||||
}
|
||||
insert_case5(t, n)
|
||||
}
|
||||
|
||||
@(private)
|
||||
insert_case5 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
n._parent._color = .Black
|
||||
grand_parent(n)._color = .Red
|
||||
if n == n._parent._left && n._parent == grand_parent(n)._left {
|
||||
rotate__right(t, grand_parent(n))
|
||||
} else {
|
||||
rotate__left(t, grand_parent(n))
|
||||
}
|
||||
}
|
||||
|
||||
// The maximum_node() helper function just walks _right until it reaches the last non-leaf:
|
||||
@(private)
|
||||
maximum_node :: proc(n: ^$N/Node($Key, $Value)) -> (max_node: ^N) {
|
||||
n := n
|
||||
for n._right != nil {
|
||||
n = n._right
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case1 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if n._parent == nil {
|
||||
return
|
||||
} else {
|
||||
remove_case2(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case2 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if node_color(sibling(n)) == .Red {
|
||||
n._parent._color = .Red
|
||||
sibling(n)._color = .Black
|
||||
if n == n._parent._left {
|
||||
rotate__left(t, n._parent)
|
||||
} else {
|
||||
rotate__right(t, n._parent)
|
||||
}
|
||||
}
|
||||
remove_case3(t, n)
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case3 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if node_color(n._parent) == .Black &&
|
||||
node_color(sibling(n)) == .Black &&
|
||||
node_color(sibling(n)._left) == .Black &&
|
||||
node_color(sibling(n)._right) == .Black {
|
||||
sibling(n)._color = .Red
|
||||
remove_case1(t, n._parent)
|
||||
} else {
|
||||
remove_case4(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case4 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if node_color(n._parent) == .Red &&
|
||||
node_color(sibling(n)) == .Black &&
|
||||
node_color(sibling(n)._left) == .Black &&
|
||||
node_color(sibling(n)._right) == .Black {
|
||||
sibling(n)._color = .Red
|
||||
n._parent._color = .Black
|
||||
} else {
|
||||
remove_case5(t, n)
|
||||
}
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case5 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
if n == n._parent._left &&
|
||||
node_color(sibling(n)) == .Black &&
|
||||
node_color(sibling(n)._left) == .Red &&
|
||||
node_color(sibling(n)._right) == .Black {
|
||||
sibling(n)._color = .Red
|
||||
sibling(n)._left._color = .Black
|
||||
rotate__right(t, sibling(n))
|
||||
} else if n == n._parent._right &&
|
||||
node_color(sibling(n)) == .Black &&
|
||||
node_color(sibling(n)._right) == .Red &&
|
||||
node_color(sibling(n)._left) == .Black {
|
||||
sibling(n)._color = .Red
|
||||
sibling(n)._right._color = .Black
|
||||
rotate__left(t, sibling(n))
|
||||
}
|
||||
remove_case6(t, n)
|
||||
}
|
||||
|
||||
@(private)
|
||||
remove_case6 :: proc(t: ^$T/Tree($Key, $Value), n: ^$N/Node(Key, Value)) {
|
||||
sibling(n)._color = node_color(n._parent)
|
||||
n._parent._color = .Black
|
||||
if n == n._parent._left {
|
||||
sibling(n)._right._color = .Black
|
||||
rotate__left(t, n._parent)
|
||||
} else {
|
||||
sibling(n)._left._color = .Black
|
||||
rotate__right(t, n._parent)
|
||||
}
|
||||
}
|
||||
|
||||
node_color :: proc(n: ^$N/Node($Key, $Value)) -> (c: Color) {
|
||||
return n == nil ? .Black : n._color
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//+build ignore
|
||||
package encoding_csv
|
||||
|
||||
import "core:fmt"
|
||||
import "core:encoding/csv"
|
||||
import "core:os"
|
||||
|
||||
// Requires keeping the entire CSV file in memory at once
|
||||
iterate_csv_from_string :: proc(filename: string) {
|
||||
r: csv.Reader
|
||||
r.trim_leading_space = true
|
||||
r.reuse_record = true // Without it you have to delete(record)
|
||||
r.reuse_record_buffer = true // Without it you have to each of the fields within it
|
||||
defer csv.reader_destroy(&r)
|
||||
|
||||
if csv_data, ok := os.read_entire_file(filename); ok {
|
||||
csv.reader_init_with_string(&r, string(csv_data))
|
||||
defer delete(csv_data)
|
||||
} else {
|
||||
fmt.printfln("Unable to open file: %v", filename)
|
||||
return
|
||||
}
|
||||
|
||||
for r, i, err in csv.iterator_next(&r) {
|
||||
if err != nil { /* Do something with error */ }
|
||||
for f, j in r {
|
||||
fmt.printfln("Record %v, field %v: %q", i, j, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the CSV as it's processed (with a small buffer)
|
||||
iterate_csv_from_stream :: proc(filename: string) {
|
||||
fmt.printfln("Hellope from %v", filename)
|
||||
r: csv.Reader
|
||||
r.trim_leading_space = true
|
||||
r.reuse_record = true // Without it you have to delete(record)
|
||||
r.reuse_record_buffer = true // Without it you have to each of the fields within it
|
||||
defer csv.reader_destroy(&r)
|
||||
|
||||
handle, errno := os.open(filename)
|
||||
if errno != os.ERROR_NONE {
|
||||
fmt.printfln("Error opening file: %v", filename)
|
||||
return
|
||||
}
|
||||
defer os.close(handle)
|
||||
csv.reader_init(&r, os.stream_from_handle(handle))
|
||||
|
||||
for r, i in csv.iterator_next(&r) {
|
||||
for f, j in r {
|
||||
fmt.printfln("Record %v, field %v: %q", i, j, f)
|
||||
}
|
||||
}
|
||||
fmt.printfln("Error: %v", csv.iterator_last_error(r))
|
||||
}
|
||||
|
||||
// Read all records at once
|
||||
read_csv_from_string :: proc(filename: string) {
|
||||
r: csv.Reader
|
||||
r.trim_leading_space = true
|
||||
r.reuse_record = true // Without it you have to delete(record)
|
||||
r.reuse_record_buffer = true // Without it you have to each of the fields within it
|
||||
defer csv.reader_destroy(&r)
|
||||
|
||||
if csv_data, ok := os.read_entire_file(filename); ok {
|
||||
csv.reader_init_with_string(&r, string(csv_data))
|
||||
defer delete(csv_data)
|
||||
} else {
|
||||
fmt.printfln("Unable to open file: %v", filename)
|
||||
return
|
||||
}
|
||||
|
||||
records, err := csv.read_all(&r)
|
||||
if err != nil { /* Do something with CSV parse error */ }
|
||||
|
||||
defer {
|
||||
for rec in records {
|
||||
delete(rec)
|
||||
}
|
||||
delete(records)
|
||||
}
|
||||
|
||||
for r, i in records {
|
||||
for f, j in r {
|
||||
fmt.printfln("Record %v, field %v: %q", i, j, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ Reader :: struct {
|
||||
field_indices: [dynamic]int,
|
||||
last_record: [dynamic]string,
|
||||
sr: strings.Reader, // used by reader_init_with_string
|
||||
|
||||
// Set and used by the iterator. Query using `iterator_last_error`
|
||||
last_iterator_error: Error,
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +124,25 @@ reader_destroy :: proc(r: ^Reader) {
|
||||
bufio.reader_destroy(&r.r)
|
||||
}
|
||||
|
||||
/*
|
||||
Returns a record at a time.
|
||||
|
||||
for record, row_idx in csv.iterator_next(&r) { ... }
|
||||
|
||||
TIP: If you process the results within the loop and don't need to own the results,
|
||||
you can set the Reader's `reuse_record` and `reuse_record_reuse_record_buffer` to true;
|
||||
you won't need to delete the record or its fields.
|
||||
*/
|
||||
iterator_next :: proc(r: ^Reader) -> (record: []string, idx: int, err: Error, more: bool) {
|
||||
record, r.last_iterator_error = read(r)
|
||||
return record, r.line_count - 1, r.last_iterator_error, r.last_iterator_error == nil
|
||||
}
|
||||
|
||||
// Get last error if we the iterator
|
||||
iterator_last_error :: proc(r: Reader) -> (err: Error) {
|
||||
return r.last_iterator_error
|
||||
}
|
||||
|
||||
// read reads a single record (a slice of fields) from r
|
||||
//
|
||||
// All \r\n sequences are normalized to \n, including multi-line field
|
||||
@@ -460,5 +482,4 @@ _read_record :: proc(r: ^Reader, dst: ^[dynamic]string, allocator := context.all
|
||||
r.fields_per_record = len(dst)
|
||||
}
|
||||
return dst[:], err
|
||||
|
||||
}
|
||||
}
|
||||
@@ -539,8 +539,6 @@ marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err:
|
||||
case: panic("unknown bit_size size")
|
||||
}
|
||||
io.write_u64(w, bit_data) or_return
|
||||
|
||||
return .Unsupported_Type
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
+148
-54
@@ -13,20 +13,7 @@ import "core:unicode/utf8"
|
||||
|
||||
// Internal data structure that stores the required information for formatted printing
|
||||
Info :: struct {
|
||||
minus: bool,
|
||||
plus: bool,
|
||||
space: bool,
|
||||
zero: bool,
|
||||
hash: bool,
|
||||
width_set: bool,
|
||||
prec_set: bool,
|
||||
|
||||
width: int,
|
||||
prec: int,
|
||||
indent: int,
|
||||
|
||||
ignore_user_formatters: bool,
|
||||
in_bad: bool,
|
||||
using state: Info_State,
|
||||
|
||||
writer: io.Writer,
|
||||
arg: any, // Temporary
|
||||
@@ -39,6 +26,24 @@ Info :: struct {
|
||||
n: int, // bytes written
|
||||
}
|
||||
|
||||
Info_State :: struct {
|
||||
minus: bool,
|
||||
plus: bool,
|
||||
space: bool,
|
||||
zero: bool,
|
||||
hash: bool,
|
||||
width_set: bool,
|
||||
prec_set: bool,
|
||||
|
||||
ignore_user_formatters: bool,
|
||||
in_bad: bool,
|
||||
|
||||
width: int,
|
||||
prec: int,
|
||||
indent: int,
|
||||
}
|
||||
|
||||
|
||||
// Custom formatter signature. It returns true if the formatting was successful and false when it could not be done
|
||||
User_Formatter :: #type proc(fi: ^Info, arg: any, verb: rune) -> bool
|
||||
|
||||
@@ -994,6 +999,33 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
|
||||
}
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
if fi.hash && !is_signed {
|
||||
switch base {
|
||||
case 2:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'b', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 8:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 12:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 16:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'x', &fi.n)
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
prec := 0
|
||||
if fi.prec_set {
|
||||
prec = fi.prec
|
||||
@@ -1019,14 +1051,10 @@ _fmt_int :: proc(fi: ^Info, u: u64, base: int, is_signed: bool, bit_size: int, d
|
||||
panic("_fmt_int: unknown base, whoops")
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
flags: strconv.Int_Flags
|
||||
if fi.hash { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
s := strconv.append_bits(buf[start:], u, base, is_signed, bit_size, digits, flags)
|
||||
|
||||
prev_zero := fi.zero
|
||||
defer fi.zero = prev_zero
|
||||
fi.zero = false
|
||||
@@ -1056,6 +1084,33 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
|
||||
}
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
if fi.hash && !is_signed {
|
||||
switch base {
|
||||
case 2:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'b', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 8:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 12:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'o', &fi.n)
|
||||
start = 2
|
||||
|
||||
case 16:
|
||||
io.write_byte(fi.writer, '0', &fi.n)
|
||||
io.write_byte(fi.writer, 'x', &fi.n)
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
prec := 0
|
||||
if fi.prec_set {
|
||||
prec = fi.prec
|
||||
@@ -1081,12 +1136,9 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i
|
||||
panic("_fmt_int: unknown base, whoops")
|
||||
}
|
||||
|
||||
buf: [256]byte
|
||||
start := 0
|
||||
|
||||
flags: strconv.Int_Flags
|
||||
if fi.hash && !fi.zero { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
if fi.hash && !fi.zero && start == 0 { flags |= {.Prefix} }
|
||||
if fi.plus { flags |= {.Plus} }
|
||||
s := strconv.append_bits_128(buf[start:], u, base, is_signed, bit_size, digits, flags)
|
||||
|
||||
if fi.hash && fi.zero && fi.indent == 0 {
|
||||
@@ -1777,7 +1829,7 @@ fmt_write_array :: proc(fi: ^Info, array_data: rawptr, count: int, elem_size: in
|
||||
// Returns: A boolean value indicating whether to continue processing the tag
|
||||
//
|
||||
@(private)
|
||||
handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
|
||||
handle_tag :: proc(state: ^Info_State, data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb: ^rune, optional_len: ^int, use_nul_termination: ^bool) -> (do_continue: bool) {
|
||||
handle_optional_len :: proc(data: rawptr, info: reflect.Type_Info_Struct, field_name: string, optional_len: ^int) {
|
||||
if optional_len == nil {
|
||||
return
|
||||
@@ -1794,45 +1846,83 @@ handle_tag :: proc(data: rawptr, info: reflect.Type_Info_Struct, idx: int, verb:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tag := info.tags[idx]
|
||||
if vt, ok := reflect.struct_tag_lookup(reflect.Struct_Tag(tag), "fmt"); ok {
|
||||
value := strings.trim_space(string(vt))
|
||||
switch value {
|
||||
case "": return false
|
||||
case "": return false
|
||||
case "-": return true
|
||||
}
|
||||
r, w := utf8.decode_rune_in_string(value)
|
||||
value = value[w:]
|
||||
if value == "" || value[0] == ',' {
|
||||
if verb^ == 'w' {
|
||||
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
|
||||
switch r {
|
||||
case 's': r = 'q'
|
||||
case: r = 'w'
|
||||
}
|
||||
|
||||
fi := state
|
||||
|
||||
head, _, tail := strings.partition(value, ",")
|
||||
|
||||
i := 0
|
||||
prefix_loop: for ; i < len(head); i += 1 {
|
||||
switch head[i] {
|
||||
case '+':
|
||||
fi.plus = true
|
||||
case '-':
|
||||
fi.minus = true
|
||||
fi.zero = false
|
||||
case ' ':
|
||||
fi.space = true
|
||||
case '#':
|
||||
fi.hash = true
|
||||
case '0':
|
||||
fi.zero = !fi.minus
|
||||
case:
|
||||
break prefix_loop
|
||||
}
|
||||
verb^ = r
|
||||
if len(value) > 0 && value[0] == ',' {
|
||||
field_name := value[1:]
|
||||
if field_name == "0" {
|
||||
if use_nul_termination != nil {
|
||||
use_nul_termination^ = true
|
||||
}
|
||||
} else {
|
||||
switch r {
|
||||
case 's', 'q':
|
||||
}
|
||||
|
||||
fi.width, i, fi.width_set = _parse_int(head, i)
|
||||
if i < len(head) && head[i] == '.' {
|
||||
i += 1
|
||||
prev_i := i
|
||||
fi.prec, i, fi.prec_set = _parse_int(head, i)
|
||||
if i == prev_i {
|
||||
fi.prec = 0
|
||||
fi.prec_set = true
|
||||
}
|
||||
}
|
||||
|
||||
r: rune
|
||||
if i >= len(head) || head[i] == ' ' {
|
||||
r = 'v'
|
||||
} else {
|
||||
r, _ = utf8.decode_rune_in_string(head[i:])
|
||||
}
|
||||
if verb^ == 'w' {
|
||||
// TODO(bill): is this a good idea overriding that field tags if 'w' is used?
|
||||
switch r {
|
||||
case 's': r = 'q'
|
||||
case: r = 'w'
|
||||
}
|
||||
}
|
||||
verb^ = r
|
||||
if tail != "" {
|
||||
field_name := tail
|
||||
if field_name == "0" {
|
||||
if use_nul_termination != nil {
|
||||
use_nul_termination^ = true
|
||||
}
|
||||
} else {
|
||||
switch r {
|
||||
case 's', 'q':
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
case 'v', 'w':
|
||||
#partial switch reflect.type_kind(info.types[idx].id) {
|
||||
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
case 'v', 'w':
|
||||
#partial switch reflect.type_kind(info.types[idx].id) {
|
||||
case .String, .Multi_Pointer, .Array, .Slice, .Dynamic_Array:
|
||||
handle_optional_len(data, info, field_name, optional_len)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return
|
||||
}
|
||||
// Formats a struct for output, handling various struct types (e.g., SOA, raw unions)
|
||||
//
|
||||
@@ -1980,7 +2070,9 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
optional_len: int = -1
|
||||
use_nul_termination: bool = false
|
||||
verb := the_verb if the_verb == 'w' else 'v'
|
||||
if handle_tag(v.data, info, i, &verb, &optional_len, &use_nul_termination) {
|
||||
|
||||
new_state := fi.state
|
||||
if handle_tag(&new_state, v.data, info, i, &verb, &optional_len, &use_nul_termination) {
|
||||
continue
|
||||
}
|
||||
field_count += 1
|
||||
@@ -2005,8 +2097,11 @@ fmt_struct :: proc(fi: ^Info, v: any, the_verb: rune, info: runtime.Type_Info_St
|
||||
if t := info.types[i]; reflect.is_any(t) {
|
||||
io.write_string(fi.writer, "any{}", &fi.n)
|
||||
} else {
|
||||
prev_state := fi.state
|
||||
fi.state = new_state
|
||||
data := rawptr(uintptr(v.data) + info.offsets[i])
|
||||
fmt_arg(fi, any{data, t.id}, verb)
|
||||
fi.state = prev_state
|
||||
}
|
||||
|
||||
if do_trailing_comma { io.write_string(fi.writer, ",\n", &fi.n) }
|
||||
@@ -2679,7 +2774,6 @@ fmt_value :: proc(fi: ^Info, v: any, verb: rune) {
|
||||
io.write_byte(fi.writer, '[' if verb != 'w' else '{', &fi.n)
|
||||
io.write_byte(fi.writer, '\n', &fi.n)
|
||||
defer {
|
||||
io.write_byte(fi.writer, '\n', &fi.n)
|
||||
fmt_write_indent(fi)
|
||||
io.write_byte(fi.writer, ']' if verb != 'w' else '}', &fi.n)
|
||||
}
|
||||
|
||||
+377
-380
@@ -1,381 +1,378 @@
|
||||
/*
|
||||
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||
Made available under Odin's BSD-3 license.
|
||||
|
||||
List of contributors:
|
||||
Jeroen van Rijn: Initial implementation.
|
||||
*/
|
||||
|
||||
|
||||
// package qoi implements a QOI image reader
|
||||
//
|
||||
// The QOI specification is at https://qoiformat.org.
|
||||
package qoi
|
||||
|
||||
import "core:image"
|
||||
import "core:compress"
|
||||
import "core:bytes"
|
||||
|
||||
Error :: image.Error
|
||||
Image :: image.Image
|
||||
Options :: image.Options
|
||||
|
||||
RGB_Pixel :: image.RGB_Pixel
|
||||
RGBA_Pixel :: image.RGBA_Pixel
|
||||
|
||||
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
if img == nil {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if output == nil {
|
||||
return .Invalid_Output
|
||||
}
|
||||
|
||||
pixels := img.width * img.height
|
||||
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
// QOI supports only 8-bit images with 3 or 4 channels.
|
||||
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if img.channels * pixels != len(img.pixels.buf) {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
written := 0
|
||||
|
||||
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
|
||||
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
|
||||
|
||||
if resize(&output.buf, max_size) != nil {
|
||||
return .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
header := image.QOI_Header{
|
||||
magic = image.QOI_Magic,
|
||||
width = u32be(img.width),
|
||||
height = u32be(img.height),
|
||||
channels = u8(img.channels),
|
||||
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
|
||||
}
|
||||
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
|
||||
|
||||
copy(output.buf[written:], header_bytes[:])
|
||||
written += size_of(image.QOI_Header)
|
||||
|
||||
/*
|
||||
Encode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
prev := pix
|
||||
|
||||
seen[qoi_hash(pix)] = pix
|
||||
|
||||
input := img.pixels.buf[:]
|
||||
run := u8(0)
|
||||
|
||||
for len(input) > 0 {
|
||||
if img.channels == 4 {
|
||||
pix = (^RGBA_Pixel)(raw_data(input))^
|
||||
} else {
|
||||
pix.rgb = (^RGB_Pixel)(raw_data(input))^
|
||||
}
|
||||
input = input[img.channels:]
|
||||
|
||||
if pix == prev {
|
||||
run += 1
|
||||
// As long as the pixel matches the last one, accumulate the run total.
|
||||
// If we reach the max run length or the end of the image, write the run.
|
||||
if run == 62 || len(input) == 0 {
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
} else {
|
||||
if run > 0 {
|
||||
// The pixel differs from the previous one, but we still need to write the pending run.
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
|
||||
index := qoi_hash(pix)
|
||||
|
||||
if seen[index] == pix {
|
||||
// Write indexed pixel
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
|
||||
written += 1
|
||||
} else {
|
||||
// Add pixel to index
|
||||
seen[index] = pix
|
||||
|
||||
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
|
||||
if pix.a == prev.a {
|
||||
// Delta
|
||||
d := pix.rgb - prev.rgb
|
||||
|
||||
// DIFF, biased and modulo 256
|
||||
_d := d + 2
|
||||
|
||||
// LUMA, biased and modulo 256
|
||||
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
|
||||
|
||||
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
|
||||
// Delta is between -2 and 1 inclusive
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
|
||||
written += 1
|
||||
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
|
||||
// Biased luma is between {-8..7, -32..31, -8..7}
|
||||
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
|
||||
output.buf[written + 1] = _l.r << 4 | _l.b
|
||||
written += 2
|
||||
} else {
|
||||
// Write RGB literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:3])
|
||||
written += 4
|
||||
}
|
||||
} else {
|
||||
// Write RGBA literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:])
|
||||
written += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = pix
|
||||
}
|
||||
|
||||
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
|
||||
copy(output.buf[written:], trailer[:])
|
||||
written += len(trailer)
|
||||
|
||||
resize(&output.buf, written)
|
||||
return nil
|
||||
}
|
||||
|
||||
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
ctx := &compress.Context_Memory_Input{
|
||||
input_data = data,
|
||||
}
|
||||
|
||||
img, err = load_from_context(ctx, options, allocator)
|
||||
return img, err
|
||||
}
|
||||
|
||||
@(optimization_mode="speed")
|
||||
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
options := options
|
||||
|
||||
if .info in options {
|
||||
options |= {.return_metadata, .do_not_decompress_image}
|
||||
options -= {.info}
|
||||
}
|
||||
|
||||
if .return_header in options && .return_metadata in options {
|
||||
options -= {.return_header}
|
||||
}
|
||||
|
||||
header := image.read_data(ctx, image.QOI_Header) or_return
|
||||
if header.magic != image.QOI_Magic {
|
||||
return img, .Invalid_Signature
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
img = new(Image)
|
||||
}
|
||||
img.which = .QOI
|
||||
|
||||
if .return_metadata in options {
|
||||
info := new(image.QOI_Info)
|
||||
info.header = header
|
||||
img.metadata = info
|
||||
}
|
||||
|
||||
if header.channels != 3 && header.channels != 4 {
|
||||
return img, .Invalid_Number_Of_Channels
|
||||
}
|
||||
|
||||
if header.color_space != .sRGB && header.color_space != .Linear {
|
||||
return img, .Invalid_Color_Space
|
||||
}
|
||||
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return img, .Invalid_Image_Dimensions
|
||||
}
|
||||
|
||||
total_pixels := header.width * header.height
|
||||
if total_pixels > image.MAX_DIMENSIONS {
|
||||
return img, .Image_Dimensions_Too_Large
|
||||
}
|
||||
|
||||
img.width = int(header.width)
|
||||
img.height = int(header.height)
|
||||
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
|
||||
img.depth = 8
|
||||
|
||||
if .do_not_decompress_image in options {
|
||||
img.channels = int(header.channels)
|
||||
return
|
||||
}
|
||||
|
||||
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
|
||||
|
||||
if resize(&img.pixels.buf, bytes_needed) != nil {
|
||||
return img, .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
/*
|
||||
Decode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
seen[qoi_hash(pix)] = pix
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
decode: for len(pixels) > 0 {
|
||||
data := image.read_u8(ctx) or_return
|
||||
|
||||
tag := QOI_Opcode_Tag(data)
|
||||
#partial switch tag {
|
||||
case .RGB:
|
||||
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RGBA:
|
||||
pix = image.read_data(ctx, RGBA_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case:
|
||||
// 2-bit tag
|
||||
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
|
||||
#partial switch tag {
|
||||
case .INDEX:
|
||||
pix = seen[data & 63]
|
||||
|
||||
case .DIFF:
|
||||
diff_r := ((data >> 4) & 3) - 2
|
||||
diff_g := ((data >> 2) & 3) - 2
|
||||
diff_b := ((data >> 0) & 3) - 2
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .LUMA:
|
||||
data2 := image.read_u8(ctx) or_return
|
||||
|
||||
diff_g := (data & 63) - 32
|
||||
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
|
||||
diff_b := diff_g - 8 + (data2 & 15)
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RUN:
|
||||
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
|
||||
return img, .Corrupt
|
||||
} else {
|
||||
#no_bounds_check for _ in 0..<length {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
continue decode
|
||||
|
||||
case:
|
||||
unreachable()
|
||||
}
|
||||
}
|
||||
|
||||
#no_bounds_check {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
|
||||
trailer, trailer_err := compress.read_data(ctx, u64be)
|
||||
if trailer_err != nil || trailer != 0x1 {
|
||||
return img, .Missing_Or_Corrupt_Trailer
|
||||
}
|
||||
|
||||
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
|
||||
return img, .Post_Processing_Error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cleanup of image-specific data.
|
||||
*/
|
||||
destroy :: proc(img: ^Image) {
|
||||
if img == nil {
|
||||
/*
|
||||
Nothing to do.
|
||||
Load must've returned with an error.
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
bytes.buffer_destroy(&img.pixels)
|
||||
|
||||
if v, ok := img.metadata.(^image.QOI_Info); ok {
|
||||
free(v)
|
||||
}
|
||||
free(img)
|
||||
}
|
||||
|
||||
QOI_Opcode_Tag :: enum u8 {
|
||||
// 2-bit tags
|
||||
INDEX = 0b0000_0000, // 6-bit index into color array follows
|
||||
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
|
||||
LUMA = 0b1000_0000, // Luma difference
|
||||
RUN = 0b1100_0000, // Run length encoding, bias -1
|
||||
|
||||
// 8-bit tags
|
||||
RGB = 0b1111_1110, // Raw RGB pixel follows
|
||||
RGBA = 0b1111_1111, // Raw RGBA pixel follows
|
||||
}
|
||||
|
||||
QOI_Opcode_Mask :: 0b1100_0000
|
||||
QOI_Data_Mask :: 0b0011_1111
|
||||
|
||||
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
|
||||
i1 := u16(pixel.r) * 3
|
||||
i2 := u16(pixel.g) * 5
|
||||
i3 := u16(pixel.b) * 7
|
||||
i4 := u16(pixel.a) * 11
|
||||
|
||||
return u8((i1 + i2 + i3 + i4) & 63)
|
||||
}
|
||||
|
||||
@(init, private)
|
||||
_register :: proc() {
|
||||
image.register(.QOI, load_from_bytes, destroy)
|
||||
/*
|
||||
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||
Made available under Odin's BSD-3 license.
|
||||
|
||||
List of contributors:
|
||||
Jeroen van Rijn: Initial implementation.
|
||||
*/
|
||||
|
||||
|
||||
// package qoi implements a QOI image reader
|
||||
//
|
||||
// The QOI specification is at https://qoiformat.org.
|
||||
package qoi
|
||||
|
||||
import "core:image"
|
||||
import "core:compress"
|
||||
import "core:bytes"
|
||||
|
||||
Error :: image.Error
|
||||
Image :: image.Image
|
||||
Options :: image.Options
|
||||
|
||||
RGB_Pixel :: image.RGB_Pixel
|
||||
RGBA_Pixel :: image.RGBA_Pixel
|
||||
|
||||
save_to_buffer :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
if img == nil {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if output == nil {
|
||||
return .Invalid_Output
|
||||
}
|
||||
|
||||
pixels := img.width * img.height
|
||||
if pixels == 0 || pixels > image.MAX_DIMENSIONS {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
// QOI supports only 8-bit images with 3 or 4 channels.
|
||||
if img.depth != 8 || img.channels < 3 || img.channels > 4 {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
if img.channels * pixels != len(img.pixels.buf) {
|
||||
return .Invalid_Input_Image
|
||||
}
|
||||
|
||||
written := 0
|
||||
|
||||
// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
|
||||
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
|
||||
|
||||
if resize(&output.buf, max_size) != nil {
|
||||
return .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
header := image.QOI_Header{
|
||||
magic = image.QOI_Magic,
|
||||
width = u32be(img.width),
|
||||
height = u32be(img.height),
|
||||
channels = u8(img.channels),
|
||||
color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
|
||||
}
|
||||
header_bytes := transmute([size_of(image.QOI_Header)]u8)header
|
||||
|
||||
copy(output.buf[written:], header_bytes[:])
|
||||
written += size_of(image.QOI_Header)
|
||||
|
||||
/*
|
||||
Encode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
prev := pix
|
||||
|
||||
input := img.pixels.buf[:]
|
||||
run := u8(0)
|
||||
|
||||
for len(input) > 0 {
|
||||
if img.channels == 4 {
|
||||
pix = (^RGBA_Pixel)(raw_data(input))^
|
||||
} else {
|
||||
pix.rgb = (^RGB_Pixel)(raw_data(input))^
|
||||
}
|
||||
input = input[img.channels:]
|
||||
|
||||
if pix == prev {
|
||||
run += 1
|
||||
// As long as the pixel matches the last one, accumulate the run total.
|
||||
// If we reach the max run length or the end of the image, write the run.
|
||||
if run == 62 || len(input) == 0 {
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
} else {
|
||||
if run > 0 {
|
||||
// The pixel differs from the previous one, but we still need to write the pending run.
|
||||
// Encode and write run
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
|
||||
written += 1
|
||||
run = 0
|
||||
}
|
||||
|
||||
index := qoi_hash(pix)
|
||||
|
||||
if seen[index] == pix {
|
||||
// Write indexed pixel
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
|
||||
written += 1
|
||||
} else {
|
||||
// Add pixel to index
|
||||
seen[index] = pix
|
||||
|
||||
// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
|
||||
if pix.a == prev.a {
|
||||
// Delta
|
||||
d := pix.rgb - prev.rgb
|
||||
|
||||
// DIFF, biased and modulo 256
|
||||
_d := d + 2
|
||||
|
||||
// LUMA, biased and modulo 256
|
||||
_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
|
||||
|
||||
if _d.r < 4 && _d.g < 4 && _d.b < 4 {
|
||||
// Delta is between -2 and 1 inclusive
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
|
||||
written += 1
|
||||
} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
|
||||
// Biased luma is between {-8..7, -32..31, -8..7}
|
||||
output.buf[written ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
|
||||
output.buf[written + 1] = _l.r << 4 | _l.b
|
||||
written += 2
|
||||
} else {
|
||||
// Write RGB literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGB)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:3])
|
||||
written += 4
|
||||
}
|
||||
} else {
|
||||
// Write RGBA literal
|
||||
output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
|
||||
pix_bytes := transmute([4]u8)pix
|
||||
copy(output.buf[written + 1:], pix_bytes[:])
|
||||
written += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = pix
|
||||
}
|
||||
|
||||
trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
|
||||
copy(output.buf[written:], trailer[:])
|
||||
written += len(trailer)
|
||||
|
||||
resize(&output.buf, written)
|
||||
return nil
|
||||
}
|
||||
|
||||
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
ctx := &compress.Context_Memory_Input{
|
||||
input_data = data,
|
||||
}
|
||||
|
||||
img, err = load_from_context(ctx, options, allocator)
|
||||
return img, err
|
||||
}
|
||||
|
||||
@(optimization_mode="speed")
|
||||
load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
options := options
|
||||
|
||||
if .info in options {
|
||||
options |= {.return_metadata, .do_not_decompress_image}
|
||||
options -= {.info}
|
||||
}
|
||||
|
||||
if .return_header in options && .return_metadata in options {
|
||||
options -= {.return_header}
|
||||
}
|
||||
|
||||
header := image.read_data(ctx, image.QOI_Header) or_return
|
||||
if header.magic != image.QOI_Magic {
|
||||
return img, .Invalid_Signature
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
img = new(Image)
|
||||
}
|
||||
img.which = .QOI
|
||||
|
||||
if .return_metadata in options {
|
||||
info := new(image.QOI_Info)
|
||||
info.header = header
|
||||
img.metadata = info
|
||||
}
|
||||
|
||||
if header.channels != 3 && header.channels != 4 {
|
||||
return img, .Invalid_Number_Of_Channels
|
||||
}
|
||||
|
||||
if header.color_space != .sRGB && header.color_space != .Linear {
|
||||
return img, .Invalid_Color_Space
|
||||
}
|
||||
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return img, .Invalid_Image_Dimensions
|
||||
}
|
||||
|
||||
total_pixels := header.width * header.height
|
||||
if total_pixels > image.MAX_DIMENSIONS {
|
||||
return img, .Image_Dimensions_Too_Large
|
||||
}
|
||||
|
||||
img.width = int(header.width)
|
||||
img.height = int(header.height)
|
||||
img.channels = 4 if .alpha_add_if_missing in options else int(header.channels)
|
||||
img.depth = 8
|
||||
|
||||
if .do_not_decompress_image in options {
|
||||
img.channels = int(header.channels)
|
||||
return
|
||||
}
|
||||
|
||||
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
|
||||
|
||||
if resize(&img.pixels.buf, bytes_needed) != nil {
|
||||
return img, .Unable_To_Allocate_Or_Resize
|
||||
}
|
||||
|
||||
/*
|
||||
Decode loop starts here.
|
||||
*/
|
||||
seen: [64]RGBA_Pixel
|
||||
pix := RGBA_Pixel{0, 0, 0, 255}
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
decode: for len(pixels) > 0 {
|
||||
data := image.read_u8(ctx) or_return
|
||||
|
||||
tag := QOI_Opcode_Tag(data)
|
||||
#partial switch tag {
|
||||
case .RGB:
|
||||
pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RGBA:
|
||||
pix = image.read_data(ctx, RGBA_Pixel) or_return
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case:
|
||||
// 2-bit tag
|
||||
tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
|
||||
#partial switch tag {
|
||||
case .INDEX:
|
||||
pix = seen[data & 63]
|
||||
|
||||
case .DIFF:
|
||||
diff_r := ((data >> 4) & 3) - 2
|
||||
diff_g := ((data >> 2) & 3) - 2
|
||||
diff_b := ((data >> 0) & 3) - 2
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .LUMA:
|
||||
data2 := image.read_u8(ctx) or_return
|
||||
|
||||
diff_g := (data & 63) - 32
|
||||
diff_r := diff_g - 8 + ((data2 >> 4) & 15)
|
||||
diff_b := diff_g - 8 + (data2 & 15)
|
||||
|
||||
pix += {diff_r, diff_g, diff_b, 0}
|
||||
|
||||
#no_bounds_check {
|
||||
seen[qoi_hash(pix)] = pix
|
||||
}
|
||||
|
||||
case .RUN:
|
||||
if length := int(data & 63) + 1; (length * img.channels) > len(pixels) {
|
||||
return img, .Corrupt
|
||||
} else {
|
||||
#no_bounds_check for _ in 0..<length {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
continue decode
|
||||
|
||||
case:
|
||||
unreachable()
|
||||
}
|
||||
}
|
||||
|
||||
#no_bounds_check {
|
||||
copy(pixels, pix[:img.channels])
|
||||
pixels = pixels[img.channels:]
|
||||
}
|
||||
}
|
||||
|
||||
// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
|
||||
trailer, trailer_err := compress.read_data(ctx, u64be)
|
||||
if trailer_err != nil || trailer != 0x1 {
|
||||
return img, .Missing_Or_Corrupt_Trailer
|
||||
}
|
||||
|
||||
if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
|
||||
return img, .Post_Processing_Error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cleanup of image-specific data.
|
||||
*/
|
||||
destroy :: proc(img: ^Image) {
|
||||
if img == nil {
|
||||
/*
|
||||
Nothing to do.
|
||||
Load must've returned with an error.
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
bytes.buffer_destroy(&img.pixels)
|
||||
|
||||
if v, ok := img.metadata.(^image.QOI_Info); ok {
|
||||
free(v)
|
||||
}
|
||||
free(img)
|
||||
}
|
||||
|
||||
QOI_Opcode_Tag :: enum u8 {
|
||||
// 2-bit tags
|
||||
INDEX = 0b0000_0000, // 6-bit index into color array follows
|
||||
DIFF = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
|
||||
LUMA = 0b1000_0000, // Luma difference
|
||||
RUN = 0b1100_0000, // Run length encoding, bias -1
|
||||
|
||||
// 8-bit tags
|
||||
RGB = 0b1111_1110, // Raw RGB pixel follows
|
||||
RGBA = 0b1111_1111, // Raw RGBA pixel follows
|
||||
}
|
||||
|
||||
QOI_Opcode_Mask :: 0b1100_0000
|
||||
QOI_Data_Mask :: 0b0011_1111
|
||||
|
||||
qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
|
||||
i1 := u16(pixel.r) * 3
|
||||
i2 := u16(pixel.g) * 5
|
||||
i3 := u16(pixel.b) * 7
|
||||
i4 := u16(pixel.a) * 11
|
||||
|
||||
return u8((i1 + i2 + i3 + i4) & 63)
|
||||
}
|
||||
|
||||
@(init, private)
|
||||
_register :: proc() {
|
||||
image.register(.QOI, load_from_bytes, destroy)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package odin_format
|
||||
|
||||
#panic("The format package has been deprecated. Please look at https://github.com/DanielGavin/ols")
|
||||
@@ -1,41 +0,0 @@
|
||||
package odin_format
|
||||
|
||||
import "core:odin/printer"
|
||||
import "core:odin/parser"
|
||||
import "core:odin/ast"
|
||||
|
||||
default_style := printer.default_style
|
||||
|
||||
simplify :: proc(file: ^ast.File) {
|
||||
|
||||
}
|
||||
|
||||
format :: proc(filepath: string, source: string, config: printer.Config, parser_flags := parser.Flags{}, allocator := context.allocator) -> (string, bool) {
|
||||
config := config
|
||||
|
||||
pkg := ast.Package {
|
||||
kind = .Normal,
|
||||
}
|
||||
|
||||
file := ast.File {
|
||||
pkg = &pkg,
|
||||
src = source,
|
||||
fullpath = filepath,
|
||||
}
|
||||
|
||||
config.newline_limit = clamp(config.newline_limit, 0, 16)
|
||||
config.spaces = clamp(config.spaces, 1, 16)
|
||||
config.align_length_break = clamp(config.align_length_break, 0, 64)
|
||||
|
||||
p := parser.default_parser(parser_flags)
|
||||
|
||||
ok := parser.parse_file(&p, &file)
|
||||
|
||||
if !ok || file.syntax_error_count > 0 {
|
||||
return {}, false
|
||||
}
|
||||
|
||||
prnt := printer.make_printer(config, allocator)
|
||||
|
||||
return printer.print(&prnt, &file), true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package odin_printer
|
||||
|
||||
#panic("The printer package has been deprecated. Please look at https://github.com/DanielGavin/ols")
|
||||
@@ -1,922 +0,0 @@
|
||||
package odin_printer
|
||||
|
||||
import "core:odin/ast"
|
||||
import "core:odin/tokenizer"
|
||||
import "core:strings"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
|
||||
Type_Enum :: enum {Line_Comment, Value_Decl, Switch_Stmt, Struct, Assign, Call, Enum, If, For, Proc_Lit}
|
||||
|
||||
Line_Type :: bit_set[Type_Enum]
|
||||
|
||||
/*
|
||||
Represents an unwrapped line
|
||||
*/
|
||||
Line :: struct {
|
||||
format_tokens: [dynamic]Format_Token,
|
||||
finalized: bool,
|
||||
used: bool,
|
||||
depth: int,
|
||||
types: Line_Type, //for performance, so you don't have to verify what types are in it by going through the tokens - might give problems when adding linebreaking
|
||||
}
|
||||
|
||||
/*
|
||||
Represents a singular token in a unwrapped line
|
||||
*/
|
||||
Format_Token :: struct {
|
||||
kind: tokenizer.Token_Kind,
|
||||
text: string,
|
||||
type: Type_Enum,
|
||||
spaces_before: int,
|
||||
parameter_count: int,
|
||||
}
|
||||
|
||||
Printer :: struct {
|
||||
string_builder: strings.Builder,
|
||||
config: Config,
|
||||
depth: int, //the identation depth
|
||||
comments: [dynamic]^ast.Comment_Group,
|
||||
latest_comment_index: int,
|
||||
allocator: mem.Allocator,
|
||||
file: ^ast.File,
|
||||
source_position: tokenizer.Pos,
|
||||
last_source_position: tokenizer.Pos,
|
||||
lines: [dynamic]Line, //need to look into a better data structure, one that can handle inserting lines rather than appending
|
||||
skip_semicolon: bool,
|
||||
current_line: ^Line,
|
||||
current_line_index: int,
|
||||
last_line_index: int,
|
||||
last_token: ^Format_Token,
|
||||
merge_next_token: bool,
|
||||
space_next_token: bool,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
Config :: struct {
|
||||
spaces: int, //Spaces per indentation
|
||||
newline_limit: int, //The limit of newlines between statements and declarations.
|
||||
tabs: bool, //Enable or disable tabs
|
||||
convert_do: bool, //Convert all do statements to brace blocks
|
||||
semicolons: bool, //Enable semicolons
|
||||
split_multiple_stmts: bool,
|
||||
align_switch: bool,
|
||||
brace_style: Brace_Style,
|
||||
align_assignments: bool,
|
||||
align_structs: bool,
|
||||
align_style: Alignment_Style,
|
||||
align_enums: bool,
|
||||
align_length_break: int,
|
||||
indent_cases: bool,
|
||||
newline_style: Newline_Style,
|
||||
}
|
||||
|
||||
Brace_Style :: enum {
|
||||
_1TBS,
|
||||
Allman,
|
||||
Stroustrup,
|
||||
K_And_R,
|
||||
}
|
||||
|
||||
Block_Type :: enum {
|
||||
None,
|
||||
If_Stmt,
|
||||
Proc,
|
||||
Generic,
|
||||
Comp_Lit,
|
||||
Switch_Stmt,
|
||||
}
|
||||
|
||||
Alignment_Style :: enum {
|
||||
Align_On_Type_And_Equals,
|
||||
Align_On_Colon_And_Equals,
|
||||
}
|
||||
|
||||
Newline_Style :: enum {
|
||||
CRLF,
|
||||
LF,
|
||||
}
|
||||
|
||||
default_style := Config {
|
||||
spaces = 4,
|
||||
newline_limit = 2,
|
||||
convert_do = false,
|
||||
semicolons = false,
|
||||
tabs = true,
|
||||
brace_style = ._1TBS,
|
||||
split_multiple_stmts = true,
|
||||
align_assignments = true,
|
||||
align_style = .Align_On_Type_And_Equals,
|
||||
indent_cases = false,
|
||||
align_switch = true,
|
||||
align_structs = true,
|
||||
align_enums = true,
|
||||
newline_style = .CRLF,
|
||||
align_length_break = 9,
|
||||
}
|
||||
|
||||
make_printer :: proc(config: Config, allocator := context.allocator) -> Printer {
|
||||
return {
|
||||
config = config,
|
||||
allocator = allocator,
|
||||
debug = false,
|
||||
}
|
||||
}
|
||||
|
||||
print :: proc(p: ^Printer, file: ^ast.File) -> string {
|
||||
p.comments = file.comments
|
||||
|
||||
if len(file.decls) > 0 {
|
||||
p.lines = make([dynamic]Line, 0, (file.decls[len(file.decls) - 1].end.line - file.decls[0].pos.line) * 2, context.temp_allocator)
|
||||
}
|
||||
|
||||
set_source_position(p, file.pkg_token.pos)
|
||||
|
||||
p.last_source_position.line = 1
|
||||
|
||||
set_line(p, 0)
|
||||
|
||||
push_generic_token(p, .Package, 0)
|
||||
push_ident_token(p, file.pkg_name, 1)
|
||||
|
||||
for decl in file.decls {
|
||||
visit_decl(p, cast(^ast.Decl)decl)
|
||||
}
|
||||
|
||||
if len(p.comments) > 0 {
|
||||
infinite := p.comments[len(p.comments) - 1].end
|
||||
infinite.offset = 9999999
|
||||
push_comments(p, infinite)
|
||||
}
|
||||
|
||||
fix_lines(p)
|
||||
|
||||
builder := strings.builder_make(0, 5 * mem.Megabyte, p.allocator)
|
||||
|
||||
last_line := 0
|
||||
|
||||
newline: string
|
||||
|
||||
if p.config.newline_style == .LF {
|
||||
newline = "\n"
|
||||
} else {
|
||||
newline = "\r\n"
|
||||
}
|
||||
|
||||
for line, line_index in p.lines {
|
||||
diff_line := line_index - last_line
|
||||
|
||||
for i := 0; i < diff_line; i += 1 {
|
||||
strings.write_string(&builder, newline)
|
||||
}
|
||||
|
||||
if p.config.tabs {
|
||||
for i := 0; i < line.depth; i += 1 {
|
||||
strings.write_byte(&builder, '\t')
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < line.depth * p.config.spaces; i += 1 {
|
||||
strings.write_byte(&builder, ' ')
|
||||
}
|
||||
}
|
||||
|
||||
if p.debug {
|
||||
strings.write_string(&builder, fmt.tprintf("line %v: ", line_index))
|
||||
}
|
||||
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
for i := 0; i < format_token.spaces_before; i += 1 {
|
||||
strings.write_byte(&builder, ' ')
|
||||
}
|
||||
|
||||
strings.write_string(&builder, format_token.text)
|
||||
}
|
||||
|
||||
last_line = line_index
|
||||
}
|
||||
|
||||
strings.write_string(&builder, newline)
|
||||
|
||||
return strings.to_string(builder)
|
||||
}
|
||||
|
||||
fix_lines :: proc(p: ^Printer) {
|
||||
align_var_decls(p)
|
||||
format_generic(p)
|
||||
align_comments(p) //align them last since they rely on the other alignments
|
||||
}
|
||||
|
||||
format_value_decl :: proc(p: ^Printer, index: int) {
|
||||
|
||||
eq_found := false
|
||||
eq_token: Format_Token
|
||||
eq_line: int
|
||||
largest := 0
|
||||
|
||||
found_eq: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if format_token.kind == .Eq {
|
||||
eq_token = format_token
|
||||
eq_line = line_index + index
|
||||
eq_found = true
|
||||
break found_eq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !eq_found {
|
||||
return
|
||||
}
|
||||
|
||||
align_next := false
|
||||
|
||||
//check to see if there is a binary operator in the last token(this is guaranteed by the ast visit), otherwise it's not multilined
|
||||
for line in p.lines[eq_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if align_next {
|
||||
line.format_tokens[0].spaces_before = largest + 1
|
||||
align_next = false
|
||||
}
|
||||
|
||||
kind := find_last_token(line.format_tokens).kind
|
||||
|
||||
if tokenizer.Token_Kind.B_Operator_Begin < kind && kind <= tokenizer.Token_Kind.Cmp_Or {
|
||||
align_next = true
|
||||
}
|
||||
|
||||
if !align_next {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find_last_token :: proc(format_tokens: [dynamic]Format_Token) -> Format_Token {
|
||||
|
||||
for i := len(format_tokens) - 1; i >= 0; i -= 1 {
|
||||
|
||||
if format_tokens[i].kind != .Comment {
|
||||
return format_tokens[i]
|
||||
}
|
||||
}
|
||||
|
||||
panic("not possible")
|
||||
}
|
||||
|
||||
format_assignment :: proc(p: ^Printer, index: int) {
|
||||
}
|
||||
|
||||
format_call :: proc(p: ^Printer, line_index: int, format_index: int) {
|
||||
|
||||
paren_found := false
|
||||
paren_token: Format_Token
|
||||
paren_line: int
|
||||
paren_token_index: int
|
||||
largest := 0
|
||||
|
||||
found_paren: for line, i in p.lines[line_index:] {
|
||||
for format_token, j in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if i == 0 && j < format_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Paren && format_token.type == .Call {
|
||||
paren_token = format_token
|
||||
paren_line = line_index + i
|
||||
paren_found = true
|
||||
paren_token_index = j
|
||||
break found_paren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !paren_found {
|
||||
panic("Should not be possible")
|
||||
}
|
||||
|
||||
paren_count := 1
|
||||
done := false
|
||||
|
||||
for line in p.lines[paren_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
continue
|
||||
}
|
||||
|
||||
if line_index == 0 && i <= paren_token_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Paren {
|
||||
paren_count += 1
|
||||
} else if format_token.kind == .Close_Paren {
|
||||
paren_count -= 1
|
||||
}
|
||||
|
||||
if paren_count == 0 {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if line_index != 0 {
|
||||
line.format_tokens[0].spaces_before = largest
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format_keyword_to_brace :: proc(p: ^Printer, line_index: int, format_index: int, keyword: tokenizer.Token_Kind) {
|
||||
|
||||
keyword_found := false
|
||||
keyword_token: Format_Token
|
||||
keyword_line: int
|
||||
|
||||
largest := 0
|
||||
brace_count := 0
|
||||
done := false
|
||||
|
||||
found_keyword: for line, i in p.lines[line_index:] {
|
||||
for format_token in line.format_tokens {
|
||||
|
||||
largest += len(format_token.text) + format_token.spaces_before
|
||||
|
||||
if format_token.kind == keyword {
|
||||
keyword_token = format_token
|
||||
keyword_line = line_index + i
|
||||
keyword_found = true
|
||||
break found_keyword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !keyword_found {
|
||||
panic("Should not be possible")
|
||||
}
|
||||
|
||||
for line, line_idx in p.lines[keyword_line:] {
|
||||
|
||||
if len(line.format_tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
} else if format_token.kind == .Undef {
|
||||
return
|
||||
}
|
||||
|
||||
if line_idx == 0 && i <= format_index {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Open_Brace {
|
||||
brace_count += 1
|
||||
} else if format_token.kind == .Close_Brace {
|
||||
brace_count -= 1
|
||||
}
|
||||
|
||||
if brace_count == 1 {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if line_idx != 0 {
|
||||
line.format_tokens[0].spaces_before = largest + 1
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format_generic :: proc(p: ^Printer) {
|
||||
next_struct_line := 0
|
||||
|
||||
for line, line_index in p.lines {
|
||||
|
||||
if len(line.format_tokens) <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for format_token, token_index in line.format_tokens {
|
||||
#partial switch format_token.kind {
|
||||
case .For, .If, .When, .Switch:
|
||||
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
|
||||
case .Proc:
|
||||
if format_token.type == .Proc_Lit {
|
||||
format_keyword_to_brace(p, line_index, token_index, format_token.kind)
|
||||
}
|
||||
case:
|
||||
if format_token.type == .Call {
|
||||
format_call(p, line_index, token_index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if .Switch_Stmt in line.types && p.config.align_switch {
|
||||
align_switch_stmt(p, line_index)
|
||||
}
|
||||
|
||||
if .Enum in line.types && p.config.align_enums {
|
||||
align_enum(p, line_index)
|
||||
}
|
||||
|
||||
if .Struct in line.types && p.config.align_structs && next_struct_line <= 0 {
|
||||
next_struct_line = align_struct(p, line_index)
|
||||
}
|
||||
|
||||
if .Value_Decl in line.types {
|
||||
format_value_decl(p, line_index)
|
||||
}
|
||||
|
||||
if .Assign in line.types {
|
||||
format_assignment(p, line_index)
|
||||
}
|
||||
|
||||
next_struct_line -= 1
|
||||
}
|
||||
}
|
||||
|
||||
align_var_decls :: proc(p: ^Printer) {
|
||||
|
||||
current_line: int
|
||||
current_typed: bool
|
||||
current_not_mutable: bool
|
||||
|
||||
largest_lhs := 0
|
||||
largest_rhs := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
colon_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
type_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
equal_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
|
||||
|
||||
for line, line_index in p.lines {
|
||||
|
||||
//It is only possible to align value decls that are one one line, otherwise just ignore them
|
||||
if .Value_Decl not_in line.types {
|
||||
continue
|
||||
}
|
||||
|
||||
typed := true
|
||||
not_mutable := false
|
||||
continue_flag := false
|
||||
|
||||
for i := 0; i < len(line.format_tokens); i += 1 {
|
||||
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Eq {
|
||||
typed = false
|
||||
}
|
||||
|
||||
if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Colon {
|
||||
not_mutable = true
|
||||
}
|
||||
|
||||
if line.format_tokens[i].kind == .Union ||
|
||||
line.format_tokens[i].kind == .Enum ||
|
||||
line.format_tokens[i].kind == .Struct ||
|
||||
line.format_tokens[i].kind == .For ||
|
||||
line.format_tokens[i].kind == .If ||
|
||||
line.format_tokens[i].kind == .Comment {
|
||||
continue_flag = true
|
||||
}
|
||||
|
||||
//enforced undef is always on the last line, if it exists
|
||||
if line.format_tokens[i].kind == .Proc && line.format_tokens[len(line.format_tokens)-1].kind != .Undef {
|
||||
continue_flag = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if continue_flag {
|
||||
continue
|
||||
}
|
||||
|
||||
if line_index != current_line + 1 || typed != current_typed || not_mutable != current_not_mutable {
|
||||
|
||||
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
|
||||
for colon_token in colon_tokens {
|
||||
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
|
||||
}
|
||||
} else if p.config.align_style == .Align_On_Type_And_Equals {
|
||||
for type_token in type_tokens {
|
||||
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
if current_typed {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
|
||||
}
|
||||
} else {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = 0
|
||||
}
|
||||
}
|
||||
|
||||
clear(&colon_tokens)
|
||||
clear(&type_tokens)
|
||||
clear(&equal_tokens)
|
||||
|
||||
largest_rhs = 0
|
||||
largest_lhs = 0
|
||||
current_typed = typed
|
||||
current_not_mutable = not_mutable
|
||||
}
|
||||
|
||||
current_line = line_index
|
||||
|
||||
current_token_index := 0
|
||||
lhs_length := 0
|
||||
rhs_length := 0
|
||||
|
||||
//calcuate the length of lhs of a value decl i.e. `a, b:`
|
||||
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
|
||||
|
||||
lhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
|
||||
|
||||
if line.format_tokens[current_token_index].kind == .Colon {
|
||||
append(&colon_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = lhs_length})
|
||||
|
||||
if len(line.format_tokens) > current_token_index && line.format_tokens[current_token_index + 1].kind != .Eq {
|
||||
append(&type_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index + 1], length = lhs_length})
|
||||
}
|
||||
|
||||
current_token_index += 1
|
||||
largest_lhs = max(largest_lhs, lhs_length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//calcuate the length of the rhs i.e. `[dynamic]int = 123123`
|
||||
for; current_token_index < len(line.format_tokens); current_token_index += 1 {
|
||||
|
||||
rhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before
|
||||
|
||||
if line.format_tokens[current_token_index].kind == .Eq {
|
||||
append(&equal_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = rhs_length})
|
||||
largest_rhs = max(largest_rhs, rhs_length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//repeating myself, move to sub procedure
|
||||
if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
|
||||
for colon_token in colon_tokens {
|
||||
colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
|
||||
}
|
||||
} else if p.config.align_style == .Align_On_Type_And_Equals {
|
||||
for type_token in type_tokens {
|
||||
type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
if current_typed {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
|
||||
}
|
||||
} else {
|
||||
for equal_token in equal_tokens {
|
||||
equal_token.format_token.spaces_before = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
align_switch_stmt :: proc(p: ^Printer, index: int) {
|
||||
switch_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_switch_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && switch_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_switch_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Switch {
|
||||
switch_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !switch_found {
|
||||
return
|
||||
}
|
||||
|
||||
largest := 0
|
||||
case_count := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
//find all the switch cases that are one lined
|
||||
for line in p.lines[brace_line + 1:] {
|
||||
|
||||
case_found := false
|
||||
colon_found := false
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
}
|
||||
|
||||
//this will only happen if the case is one lined
|
||||
if case_found && colon_found {
|
||||
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
|
||||
largest = max(length, largest)
|
||||
break
|
||||
}
|
||||
|
||||
if format_token.kind == .Case {
|
||||
case_found = true
|
||||
case_count += 1
|
||||
} else if format_token.kind == .Colon {
|
||||
colon_found = true
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if case_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
align_enum :: proc(p: ^Printer, index: int) {
|
||||
enum_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_enum_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && enum_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_enum_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Enum {
|
||||
enum_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !enum_found {
|
||||
return
|
||||
}
|
||||
|
||||
largest := 0
|
||||
comma_count := 0
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
for line in p.lines[brace_line + 1:] {
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
}
|
||||
|
||||
if format_token.kind == .Eq {
|
||||
append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
|
||||
largest = max(length, largest)
|
||||
break
|
||||
} else if format_token.kind == .Comma {
|
||||
comma_count += 1
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if comma_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
align_struct :: proc(p: ^Printer, index: int) -> int {
|
||||
struct_found := false
|
||||
brace_token: Format_Token
|
||||
brace_line: int
|
||||
|
||||
found_struct_brace: for line, line_index in p.lines[index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Open_Brace && struct_found {
|
||||
brace_token = format_token
|
||||
brace_line = line_index + index
|
||||
break found_struct_brace
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
break
|
||||
} else if format_token.kind == .Struct {
|
||||
struct_found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !struct_found {
|
||||
return 0
|
||||
}
|
||||
|
||||
largest := 0
|
||||
colon_count := 0
|
||||
nested := false
|
||||
seen_brace := false
|
||||
|
||||
TokenAndLength :: struct {
|
||||
format_token: ^Format_Token,
|
||||
length: int,
|
||||
}
|
||||
|
||||
format_tokens := make([]TokenAndLength, brace_token.parameter_count, context.temp_allocator)
|
||||
|
||||
if brace_token.parameter_count == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
end_line_index := 0
|
||||
|
||||
for line, line_index in p.lines[brace_line + 1:] {
|
||||
length := 0
|
||||
|
||||
for format_token, i in line.format_tokens {
|
||||
|
||||
//give up on nested structs
|
||||
if format_token.kind == .Comment {
|
||||
break
|
||||
} else if format_token.kind == .Open_Paren {
|
||||
break
|
||||
} else if format_token.kind == .Open_Brace {
|
||||
seen_brace = true
|
||||
} else if format_token.kind == .Close_Brace {
|
||||
seen_brace = false
|
||||
} else if seen_brace {
|
||||
continue
|
||||
}
|
||||
|
||||
if format_token.kind == .Colon {
|
||||
format_tokens[colon_count] = {format_token = &line.format_tokens[i + 1], length = length}
|
||||
|
||||
if format_tokens[colon_count].format_token.kind == .Struct {
|
||||
nested = true
|
||||
}
|
||||
|
||||
colon_count += 1
|
||||
largest = max(length, largest)
|
||||
}
|
||||
|
||||
length += len(format_token.text) + format_token.spaces_before
|
||||
}
|
||||
|
||||
if nested {
|
||||
end_line_index = line_index + brace_line + 1
|
||||
}
|
||||
|
||||
if colon_count >= brace_token.parameter_count {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//give up aligning nested, it never looks good
|
||||
if nested {
|
||||
for line, line_index in p.lines[end_line_index:] {
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Close_Brace {
|
||||
return end_line_index + line_index - index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for token in format_tokens {
|
||||
token.format_token.spaces_before = largest - token.length + 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
align_comments :: proc(p: ^Printer) {
|
||||
|
||||
Comment_Align_Info :: struct {
|
||||
length: int,
|
||||
begin: int,
|
||||
end: int,
|
||||
depth: int,
|
||||
}
|
||||
|
||||
comment_infos := make([dynamic]Comment_Align_Info, 0, context.temp_allocator)
|
||||
|
||||
current_info: Comment_Align_Info
|
||||
|
||||
for line, line_index in p.lines {
|
||||
if len(line.format_tokens) <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if .Line_Comment in line.types {
|
||||
if current_info.end + 1 != line_index || current_info.depth != line.depth ||
|
||||
(current_info.begin == current_info.end && current_info.length == 0) {
|
||||
|
||||
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
|
||||
append(&comment_infos, current_info)
|
||||
}
|
||||
|
||||
current_info.begin = line_index
|
||||
current_info.end = line_index
|
||||
current_info.depth = line.depth
|
||||
current_info.length = 0
|
||||
}
|
||||
|
||||
length := 0
|
||||
|
||||
for format_token in line.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
current_info.length = max(current_info.length, length)
|
||||
current_info.end = line_index
|
||||
}
|
||||
|
||||
length += format_token.spaces_before + len(format_token.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
|
||||
append(&comment_infos, current_info)
|
||||
}
|
||||
|
||||
for info in comment_infos {
|
||||
|
||||
if info.begin == info.end || info.length == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := info.begin; i <= info.end; i += 1 {
|
||||
l := p.lines[i]
|
||||
|
||||
length := 0
|
||||
|
||||
for format_token in l.format_tokens {
|
||||
if format_token.kind == .Comment {
|
||||
if len(l.format_tokens) == 1 {
|
||||
l.format_tokens[i].spaces_before = info.length + 1
|
||||
} else {
|
||||
l.format_tokens[i].spaces_before = info.length - length + 1
|
||||
}
|
||||
}
|
||||
|
||||
length += format_token.spaces_before + len(format_token.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -222,7 +222,7 @@ prefix_length :: proc(a, b: $T/[]$E) -> (n: int) where intrinsics.type_is_compar
|
||||
}
|
||||
|
||||
@(require_results)
|
||||
has_prefix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_comparable(E) {
|
||||
has_prefix :: proc(array: $T/[]$E, needle: T) -> bool where intrinsics.type_is_comparable(E) {
|
||||
n := len(needle)
|
||||
if len(array) >= n {
|
||||
return equal(array[:n], needle)
|
||||
@@ -232,7 +232,7 @@ has_prefix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_c
|
||||
|
||||
|
||||
@(require_results)
|
||||
has_suffix :: proc(array: $T/[]$E, needle: E) -> bool where intrinsics.type_is_comparable(E) {
|
||||
has_suffix :: proc(array: $T/[]$E, needle: T) -> bool where intrinsics.type_is_comparable(E) {
|
||||
array := array
|
||||
m, n := len(array), len(needle)
|
||||
if m >= n {
|
||||
|
||||
@@ -17,9 +17,6 @@ when ODIN_OS == .Darwin {
|
||||
}
|
||||
|
||||
os_sync_wait_on_address_flag :: enum u32 {
|
||||
// This flag should be used as a default flag when no other flags listed below are required.
|
||||
NONE,
|
||||
|
||||
// This flag should be used when synchronizing among multiple processes by
|
||||
// placing the @addr passed to os_sync_wait_on_address and its variants
|
||||
// in a shared memory region.
|
||||
@@ -31,15 +28,12 @@ os_sync_wait_on_address_flag :: enum u32 {
|
||||
// This flag should not be used when synchronizing among multiple threads of
|
||||
// a single process. It allows the kernel to perform performance optimizations
|
||||
// as the @addr is local to the calling process.
|
||||
SHARED,
|
||||
SHARED = 0,
|
||||
}
|
||||
|
||||
os_sync_wait_on_address_flags :: bit_set[os_sync_wait_on_address_flag; u32]
|
||||
os_sync_wait_on_address_flags :: distinct bit_set[os_sync_wait_on_address_flag; u32]
|
||||
|
||||
os_sync_wake_by_address_flag :: enum u32 {
|
||||
// This flag should be used as a default flag when no other flags listed below are required.
|
||||
NONE,
|
||||
|
||||
// This flag should be used when synchronizing among multiple processes by
|
||||
// placing the @addr passed to os_sync_wake_by_address_any and its variants
|
||||
// in a shared memory region.
|
||||
@@ -51,10 +45,10 @@ os_sync_wake_by_address_flag :: enum u32 {
|
||||
// This flag should not be used when synchronizing among multiple threads of
|
||||
// a single process. It allows the kernel to perform performance optimizations
|
||||
// as the @addr is local the calling process.
|
||||
SHARED,
|
||||
SHARED = 0,
|
||||
}
|
||||
|
||||
os_sync_wake_by_address_flags :: bit_set[os_sync_wake_by_address_flag; u32]
|
||||
os_sync_wake_by_address_flags :: distinct bit_set[os_sync_wake_by_address_flag; u32]
|
||||
|
||||
os_clockid :: enum u32 {
|
||||
MACH_ABSOLUTE_TIME = 32,
|
||||
@@ -283,7 +277,7 @@ foreign system {
|
||||
// and the shared memory specification
|
||||
// (See os_sync_wake_by_address_flags_t).
|
||||
// ENOENT : No waiter(s) found waiting on the @addr.
|
||||
os_sync_wake_by_address_any :: proc(addr: rawptr, size: uint, flags: os_sync_wait_on_address_flags) -> i32 ---
|
||||
os_sync_wake_by_address_any :: proc(addr: rawptr, size: uint, flags: os_sync_wake_by_address_flags) -> i32 ---
|
||||
|
||||
// This function is a variant of os_sync_wake_by_address_any that wakes up all waiters
|
||||
// blocked in os_sync_wait_on_address or its variants.
|
||||
@@ -305,5 +299,5 @@ foreign system {
|
||||
// In the event of an error, returns -1 with errno set to indicate the error.
|
||||
//
|
||||
// This function returns same error codes as returned by os_sync_wait_on_address.
|
||||
os_sync_wake_by_address_all :: proc(addr: rawptr, size: uint, flags: os_sync_wait_on_address_flags) -> i32 ---
|
||||
os_sync_wake_by_address_all :: proc(addr: rawptr, size: uint, flags: os_sync_wake_by_address_flags) -> i32 ---
|
||||
}
|
||||
|
||||
@@ -183,16 +183,17 @@ undo_check :: proc(s: ^State) {
|
||||
}
|
||||
|
||||
// insert text into the edit state - deletes the current selection
|
||||
input_text :: proc(s: ^State, text: string) {
|
||||
input_text :: proc(s: ^State, text: string) -> int {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
return 0
|
||||
}
|
||||
if has_selection(s) {
|
||||
selection_delete(s)
|
||||
}
|
||||
insert(s, s.selection[0], text)
|
||||
offset := s.selection[0] + len(text)
|
||||
n := insert(s, s.selection[0], text)
|
||||
offset := s.selection[0] + n
|
||||
s.selection = {offset, offset}
|
||||
return n
|
||||
}
|
||||
|
||||
// insert slice of runes into the edit state - deletes the current selection
|
||||
@@ -206,8 +207,11 @@ input_runes :: proc(s: ^State, text: []rune) {
|
||||
offset := s.selection[0]
|
||||
for r in text {
|
||||
b, w := utf8.encode_rune(r)
|
||||
insert(s, offset, string(b[:w]))
|
||||
offset += w
|
||||
n := insert(s, offset, string(b[:w]))
|
||||
offset += n
|
||||
if n != w {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.selection = {offset, offset}
|
||||
}
|
||||
@@ -219,17 +223,29 @@ input_rune :: proc(s: ^State, r: rune) {
|
||||
}
|
||||
offset := s.selection[0]
|
||||
b, w := utf8.encode_rune(r)
|
||||
insert(s, offset, string(b[:w]))
|
||||
offset += w
|
||||
n := insert(s, offset, string(b[:w]))
|
||||
offset += n
|
||||
s.selection = {offset, offset}
|
||||
}
|
||||
|
||||
// insert a single rune into the edit state - deletes the current selection
|
||||
insert :: proc(s: ^State, at: int, text: string) {
|
||||
insert :: proc(s: ^State, at: int, text: string) -> int {
|
||||
undo_check(s)
|
||||
if s.builder != nil {
|
||||
inject_at(&s.builder.buf, at, text)
|
||||
if ok, _ := inject_at(&s.builder.buf, at, text); !ok {
|
||||
n := cap(s.builder.buf) - len(s.builder.buf)
|
||||
assert(n < len(text))
|
||||
for is_continuation_byte(text[n]) {
|
||||
n -= 1
|
||||
}
|
||||
if ok2, _ := inject_at(&s.builder.buf, at, text[:n]); !ok2 {
|
||||
n = 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
return len(text)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// remove the wanted range withing, usually the selection within byte indices
|
||||
@@ -263,11 +279,12 @@ selection_delete :: proc(s: ^State) {
|
||||
s.selection = {lo, lo}
|
||||
}
|
||||
|
||||
is_continuation_byte :: proc(b: byte) -> bool {
|
||||
return b >= 0x80 && b < 0xc0
|
||||
}
|
||||
|
||||
// translates the caret position
|
||||
translate_position :: proc(s: ^State, t: Translation) -> int {
|
||||
is_continuation_byte :: proc(b: byte) -> bool {
|
||||
return b >= 0x80 && b < 0xc0
|
||||
}
|
||||
is_space :: proc(b: byte) -> bool {
|
||||
return b == ' ' || b == '\t' || b == '\n'
|
||||
}
|
||||
|
||||
+36
-17
@@ -60,10 +60,6 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
|
||||
translation.pluralize = pluralizer
|
||||
strings.intern_init(&translation.intern, allocator, allocator)
|
||||
|
||||
// Gettext MO files only have one section.
|
||||
translation.k_v[""] = {}
|
||||
section := &translation.k_v[""]
|
||||
|
||||
for n := u32(0); n < count; n += 1 {
|
||||
/*
|
||||
Grab string's original length and offset.
|
||||
@@ -83,37 +79,60 @@ parse_mo_from_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, plur
|
||||
max_offset := int(max(o_offset + o_length + 1, t_offset + t_length + 1))
|
||||
if len(data) < max_offset { return translation, .Premature_EOF }
|
||||
|
||||
key := data[o_offset:][:o_length]
|
||||
val := data[t_offset:][:t_length]
|
||||
key_data := data[o_offset:][:o_length]
|
||||
val_data := data[t_offset:][:t_length]
|
||||
|
||||
/*
|
||||
Could be a pluralized string.
|
||||
*/
|
||||
zero := []byte{0}
|
||||
keys := bytes.split(key_data, zero); defer delete(keys)
|
||||
vals := bytes.split(val_data, zero); defer delete(vals)
|
||||
|
||||
keys := bytes.split(key, zero)
|
||||
vals := bytes.split(val, zero)
|
||||
|
||||
if len(keys) != len(vals) || max(len(keys), len(vals)) > MAX_PLURALS {
|
||||
if (len(keys) != 1 && len(keys) != 2) || len(vals) > MAX_PLURALS {
|
||||
return translation, .MO_File_Incorrect_Plural_Count
|
||||
}
|
||||
|
||||
for k in keys {
|
||||
interned_key, _ := strings.intern_get(&translation.intern, string(k))
|
||||
section_name := ""
|
||||
key := string(k)
|
||||
|
||||
interned_vals := make([]string, len(keys))
|
||||
// Scan for <context>EOT<key>
|
||||
for ch, i in k {
|
||||
if ch == 0x04 {
|
||||
section_name = string(k[:i])
|
||||
key = string(k[i+1:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we merge sections, then all entries end in the "" context.
|
||||
if options.merge_sections {
|
||||
section_name = ""
|
||||
}
|
||||
|
||||
section_name, _ = strings.intern_get(&translation.intern, section_name)
|
||||
if section_name not_in translation.k_v {
|
||||
translation.k_v[section_name] = {}
|
||||
}
|
||||
|
||||
section := &translation.k_v[section_name]
|
||||
interned_key, _ := strings.intern_get(&translation.intern, string(key))
|
||||
|
||||
// Duplicate key should not be allowed.
|
||||
if interned_key in section {
|
||||
return translation, .Duplicate_Key
|
||||
}
|
||||
|
||||
interned_vals := make([]string, len(vals))
|
||||
last_val: string
|
||||
|
||||
i := 0
|
||||
for v in vals {
|
||||
for v, i in vals {
|
||||
interned_vals[i], _ = strings.intern_get(&translation.intern, string(v))
|
||||
last_val = interned_vals[i]
|
||||
i += 1
|
||||
}
|
||||
section[interned_key] = interned_vals
|
||||
}
|
||||
delete(vals)
|
||||
delete(keys)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package time
|
||||
// Parsing ISO 8601 date/time strings into time.Time.
|
||||
|
||||
import dt "core:time/datetime"
|
||||
|
||||
// Parses an ISO 8601 string and returns Time in UTC, with any UTC offset applied to it.
|
||||
// Only 4-digit years are accepted.
|
||||
// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
|
||||
// Leap seconds are smeared into 23:59:59.
|
||||
iso8601_to_time_utc :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
|
||||
offset: int
|
||||
|
||||
res, offset, consumed = iso8601_to_time_and_offset(iso_datetime, is_leap)
|
||||
res._nsec += (i64(-offset) * i64(Minute))
|
||||
return res, consumed
|
||||
}
|
||||
|
||||
// Parses an ISO 8601 string and returns Time and a UTC offset in minutes.
|
||||
// e.g. 1985-04-12T23:20:50.52Z
|
||||
// Note: Only 4-digit years are accepted.
|
||||
// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
|
||||
// Leap seconds are smeared into 23:59:59.
|
||||
iso8601_to_time_and_offset :: proc(iso_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
|
||||
moment, offset, leap_second, count := iso8601_to_components(iso_datetime)
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if is_leap != nil {
|
||||
is_leap^ = leap_second
|
||||
}
|
||||
|
||||
if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
|
||||
return {}, 0, 0
|
||||
} else {
|
||||
return _res, offset, count
|
||||
}
|
||||
}
|
||||
|
||||
// Parses an ISO 8601 string and returns Time and a UTC offset in minutes.
|
||||
// e.g. 1985-04-12T23:20:50.52Z
|
||||
// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
|
||||
iso8601_to_components :: proc(iso_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
|
||||
moment, offset, count, leap_second, ok := _iso8601_to_components(iso_datetime)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
return moment, offset, leap_second, count
|
||||
}
|
||||
|
||||
// Parses an ISO 8601 string and returns datetime.DateTime.
|
||||
// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
|
||||
@(private)
|
||||
_iso8601_to_components :: proc(iso_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) {
|
||||
// A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ
|
||||
(len(iso_datetime) >= 20) or_return
|
||||
|
||||
// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
|
||||
year := scan_digits(iso_datetime[0:], "-", 4) or_return
|
||||
month := scan_digits(iso_datetime[5:], "-", 2) or_return
|
||||
day := scan_digits(iso_datetime[8:], "Tt ", 2) or_return
|
||||
hour := scan_digits(iso_datetime[11:], ":", 2) or_return
|
||||
minute := scan_digits(iso_datetime[14:], ":", 2) or_return
|
||||
second := scan_digits(iso_datetime[17:], "", 2) or_return
|
||||
nanos := 0
|
||||
count := 19
|
||||
|
||||
// Scan fractional seconds
|
||||
if iso_datetime[count] == '.' {
|
||||
count += 1 // consume '.'
|
||||
multiplier := 100_000_000
|
||||
for digit in iso_datetime[count:] {
|
||||
if multiplier >= 1 && int(digit) >= '0' && int(digit) <= '9' {
|
||||
nanos += int(digit - '0') * multiplier
|
||||
multiplier /= 10
|
||||
count += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leap second handling
|
||||
if minute == 59 && second == 60 {
|
||||
second = 59
|
||||
is_leap = true
|
||||
}
|
||||
|
||||
err: dt.Error
|
||||
if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None {
|
||||
return {}, 0, 0, false, false
|
||||
}
|
||||
|
||||
if len(iso_datetime[count:]) == 0 {
|
||||
return res, utc_offset, count, is_leap, true
|
||||
}
|
||||
|
||||
// Scan UTC offset
|
||||
switch iso_datetime[count] {
|
||||
case 'Z', 'z':
|
||||
utc_offset = 0
|
||||
count += 1
|
||||
case '+', '-':
|
||||
(len(iso_datetime[count:]) >= 6) or_return
|
||||
offset_hour := scan_digits(iso_datetime[count+1:], ":", 2) or_return
|
||||
offset_minute := scan_digits(iso_datetime[count+4:], "", 2) or_return
|
||||
|
||||
utc_offset = 60 * offset_hour + offset_minute
|
||||
utc_offset *= -1 if iso_datetime[count] == '-' else 1
|
||||
count += 6
|
||||
}
|
||||
return res, utc_offset, count, is_leap, true
|
||||
}
|
||||
@@ -57,12 +57,12 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
|
||||
(len(rfc_datetime) >= 20) or_return
|
||||
|
||||
// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
|
||||
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
|
||||
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
|
||||
day := scan_digits(rfc_datetime[8:], "Tt", 2) or_return
|
||||
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
|
||||
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
|
||||
second := scan_digits(rfc_datetime[17:], "", 2) or_return
|
||||
year := scan_digits(rfc_datetime[0:], "-", 4) or_return
|
||||
month := scan_digits(rfc_datetime[5:], "-", 2) or_return
|
||||
day := scan_digits(rfc_datetime[8:], "Tt ", 2) or_return
|
||||
hour := scan_digits(rfc_datetime[11:], ":", 2) or_return
|
||||
minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
|
||||
second := scan_digits(rfc_datetime[17:], "", 2) or_return
|
||||
nanos := 0
|
||||
count := 19
|
||||
|
||||
@@ -87,7 +87,7 @@ _rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_o
|
||||
|
||||
// Scan UTC offset
|
||||
switch rfc_datetime[count] {
|
||||
case 'Z':
|
||||
case 'Z', 'z':
|
||||
utc_offset = 0
|
||||
count += 1
|
||||
case '+', '-':
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//+build wasi
|
||||
package time
|
||||
|
||||
import wasi "core:sys/wasm/wasi"
|
||||
|
||||
_IS_SUPPORTED :: false
|
||||
|
||||
_now :: proc "contextless" () -> Time {
|
||||
|
||||
Reference in New Issue
Block a user