diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a18e084 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +*.annot +*.cmo +*.cma +*.cmi +*.a +*.o +*.cmx +*.cmxs +*.cmxa + +# ocamlbuild working directory +_build/ + +# ocamlbuild targets +*.byte +*.native + +# oasis generated files +setup.data +setup.log + +# Merlin configuring file for Vim and Emacs +.merlin + +# Dune generated files +*.install + +# Local OPAM switch +_opam/ diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..fa4af5a --- /dev/null +++ b/.ocamlformat @@ -0,0 +1 @@ +profile=janestreet \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bacb401 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Qexat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9eaf60 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# ansifmt + +> [!CAUTION] +> Still in development. Untested. + +A simple, lightweight library for ANSI formatting with powerful features such as a tokenization-based system for pretty-printing code in the terminal. diff --git a/ansifmt.opam b/ansifmt.opam new file mode 100644 index 0000000..3ebc09c --- /dev/null +++ b/ansifmt.opam @@ -0,0 +1,31 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "A simple, lightweight library for ANSI formatting" +description: + "A simple, lightweight library for ANSI formatting with powerful features such as a tokenization-based system for pretty-printing code in the terminal." +maintainer: ["Qexat "] +authors: ["Qexat "] +license: "MIT" +tags: ["ansi" "formatting" "pretty-printing" "terminal"] +homepage: "https://github.com/qexat/ansifmt" +bug-reports: "https://github.com/qexat/ansifmt/issues" +depends: [ + "dune" {>= "3.17"} + "ocaml" + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/qexat/ansifmt.git" diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..7944e0b --- /dev/null +++ b/dune-project @@ -0,0 +1,23 @@ +(lang dune 3.17) + +(name ansifmt) + +(generate_opam_files true) + +(source + (github qexat/ansifmt)) + +(authors "Qexat ") + +(maintainers "Qexat ") + +(license MIT) + +(package + (name ansifmt) + (synopsis "A simple, lightweight library for ANSI formatting") + (description + "A simple, lightweight library for ANSI formatting with powerful features such as a tokenization-based system for pretty-printing code in the terminal.") + (depends ocaml) + (tags + ("ansi" "formatting" "pretty-printing" "terminal"))) diff --git a/lib/Ansifmt.ml b/lib/Ansifmt.ml new file mode 100644 index 0000000..c954ac8 --- /dev/null +++ b/lib/Ansifmt.ml @@ -0,0 +1,10 @@ +module Color = Color +module Formatting = Formatting +module Styling = Styling + +type color = Color.t +type styling = Styling.t +type stylizer = Formatting.Stylizer.t + +let make_styling = Styling.create +let format = Formatting.format diff --git a/lib/Color.ml b/lib/Color.ml new file mode 100644 index 0000000..41a32e4 --- /dev/null +++ b/lib/Color.ml @@ -0,0 +1,279 @@ +(** [Color] encodes ANSI colors. + + It comes in three flavors: + - 4-bit ([Minimal]) + - 8-bit ([Advanced]) + - 24-bit ([Rgb]) *) + +open Util + +module Ground = struct + (** [Ground] encodes the information on whether a certain + color is on the foreground or the background. *) + + type t = + | Foreground + | Background + + (** [to_int ?bright ground] produces the corresponding leading + digit for an SGR escape sequence to be set as foreground + or background. *) + let to_int ?(bright : bool = false) (ground : t) : int = + (* We could also match only on [ground] and add 6 when + [bright] is true, but I like to avoid arithmetic code when + possible as it makes it more obscure - constants are easier + to reason about and less error prone. *) + match ground, bright with + | Foreground, false -> 3 + | Background, false -> 4 + | Foreground, true -> 9 + | Background, true -> 10 + ;; +end + +let foreground = Ground.Foreground +let background = Ground.Background + +module Minimal = struct + (** [Minimal] encodes the 8 default ANSI colors. *) + + type t = + | Black + | Red + | Green + | Yellow + | Blue + | Magenta + | Cyan + | White + + (** [to_int color] produces the corresponding ANSI SGR code + of the [color], which is a value between 0 and 7. *) + let to_int : t -> int = function + | Black -> 0 + | Red -> 1 + | Green -> 2 + | Yellow -> 3 + | Blue -> 4 + | Magenta -> 5 + | Cyan -> 6 + | White -> 7 + ;; +end + +type t = + (* Minimal could also be a tuple, but I like the explicitness + of records. *) + | Minimal of + { color : Minimal.t + ; bright : bool + } + | Advanced of Int8.t + (* Here, I don't think a record is needed - the order of the + channels are literally given out by the constructor's name. *) + | Rgb of Int8.t * Int8.t * Int8.t + +(* It's handy for the user to have module-level constants for + each minimal color. *) + +(** Default black color. *) +let black : t = Minimal { color = Minimal.Black; bright = false } + +(** Default red color. *) +let red : t = Minimal { color = Minimal.Red; bright = false } + +(** Default green color. *) +let green : t = Minimal { color = Minimal.Green; bright = false } + +(** Default yellow color. *) +let yellow : t = Minimal { color = Minimal.Yellow; bright = false } + +(** Default blue color. *) +let blue : t = Minimal { color = Minimal.Blue; bright = false } + +(** Default magenta color. *) +let magenta : t = Minimal { color = Minimal.Magenta; bright = false } + +(** Default cyan color. *) +let cyan : t = Minimal { color = Minimal.Cyan; bright = false } + +(** Default white color. *) +let white : t = Minimal { color = Minimal.White; bright = false } + +(** Default bright black (gray) color. *) +let bright_black : t = Minimal { color = Minimal.Black; bright = true } + +(** Default bright red color. *) +let bright_red : t = Minimal { color = Minimal.Red; bright = true } + +(** Default bright green color. *) +let bright_green : t = Minimal { color = Minimal.Green; bright = true } + +(** Default bright yellow color. *) +let bright_yellow : t = Minimal { color = Minimal.Yellow; bright = true } + +(** Default bright blue color. *) +let bright_blue : t = Minimal { color = Minimal.Blue; bright = true } + +(** Default bright magenta color. *) +let bright_magenta : t = Minimal { color = Minimal.Magenta; bright = true } + +(** Default bright cyan color. *) +let bright_cyan : t = Minimal { color = Minimal.Cyan; bright = true } + +(** Default bright white color. *) +let bright_white : t = Minimal { color = Minimal.White; bright = true } + +(* It's also useful for the user to have module-level functions + to easily create colors without knowing the intricacies and + details of their implementation. *) + +(** [make_minimal ?bright value] creates a minimal color. + If [value] is not a valid color code, it returns [None]. + + A valid color code is an integer i where 0 <= i <= 7. *) +let make_minimal ?(bright : bool = false) : int -> t option = function + | 0 -> Some (Minimal { color = Minimal.Black; bright }) + | 1 -> Some (Minimal { color = Minimal.Red; bright }) + | 2 -> Some (Minimal { color = Minimal.Green; bright }) + | 3 -> Some (Minimal { color = Minimal.Yellow; bright }) + | 4 -> Some (Minimal { color = Minimal.Blue; bright }) + | 5 -> Some (Minimal { color = Minimal.Magenta; bright }) + | 6 -> Some (Minimal { color = Minimal.Cyan; bright }) + | 7 -> Some (Minimal { color = Minimal.White; bright }) + | _ -> None +;; + +(** [make_minimal_exn ?bright value] creates a minimal color. + If [value] is not a valid color code, it raises a [Failure] + exception. + + A valid color code is an integer i where 0 <= i <= 7. *) +let make_minimal_exn ?(bright : bool = false) (value : int) : t = + match make_minimal ~bright value with + | None -> failwith "value must be an integer between 0 and 7 (both included)" + | Some color -> color +;; + +(** [make_advanced value] creates an advanced color. + If [value] is not a valid color code, it returns [None]. + + A valid color code is an integer i where 0 <= i < 256. *) +let make_advanced (value : int) : t option = + Option.map (fun (value : Int8.t) -> Advanced value) (Int8.of_int value) +;; + +(** [make_advanced_exn value] creates an advanced color. + If [value] is not a valid color code, it raises a [Failure] + exception. + + A valid color code is an integer i where 0 <= i < 256. *) +let make_advanced_exn (value : int) : t = Advanced (Int8.of_int_exn value) + +module Channel = struct + (** Represents an RGB channel - either red, green, or blue. *) + type t = + | Red of int + | Green of int + | Blue of int + + (** [name channel] returns the name of the [channel]. *) + let name : t -> string = function + | Red _ -> "red" + | Green _ -> "green" + | Blue _ -> "blue" + ;; + + (** [value channel] retreives the underlying value of the + [channel]. *) + let value : t -> int = function + | Red value -> value + | Green value -> value + | Blue value -> value + ;; + + (** [to_int8 channel] tries to convert the [channel]'s value + into a [Int8] value. + + If it fails, it returns the channel data wrapped in the + [Error] variant. This is because this function is often + applied in batch to every channel of an RGB component, so + the user can trace which channel's value was invalid. *) + let to_int8 (channel : t) : (Int8.t, t) result = + Option.to_result ~none:channel (Int8.of_int (value channel)) + ;; + + (** [red value] creates a [Red] channel. *) + let red (value : int) : t = Red value + + (** [green value] creates a [Green] channel. *) + let green (value : int) : t = Green value + + (** [blue value] creates a [Blue] channel. *) + let blue (value : int) : t = Blue value +end + +(** [make_rgb red green blue] creates an RGB color. + If any of [red], [green] or [blue] is not a valid channel + value, it returns an [Error] which indicates which channel + had an invalid value. + + A valid channel value is an integer i where 0 <= i < 256. + + For the version that returns an [option] instead, see + [make_rgb_opt]. *) +let make_rgb (red : int) (green : int) (blue : int) : (t, Channel.t) result = + (* We convert each channel independently by mapping them to + some specialized type so we can extract the information of + what the first channel with an incorrect value was. + That information is used for example in [make_rgb_exn]. *) + (red, green, blue) + |> Triplet.map Channel.red Channel.green Channel.blue + |> Triplet.map_uniform ~func:Channel.to_int8 + |> Triplet.all_ok + |> Result.map (fun (r, g, b) -> Rgb (r, g, b)) +;; + +(** [make_rgb_opt red green blue] creates an RGB color. + If any of [red], [green] or [blue] is not a valid channel + value, it returns [None]. + + A valid channel value is an integer i where 0 <= i < 256. + + For the version that returns a result with the invalid + channel data, see [make_rgb]. *) +let make_rgb_opt (red : int) (green : int) (blue : int) : t option = + Result.to_option (make_rgb red green blue) +;; + +(** [make_rgb_exn red green blue] creates an RGB color. + If any of [red], [green] or [blue] is not a valid channel + value, it raises a [Failure] exception. + + A valid channel value is an integer i where 0 <= i < 256. *) +let make_rgb_exn (red : int) (green : int) (blue : int) : t = + match make_rgb red green blue with + | Ok color -> color + | Error channel -> + failwith + (Printf.sprintf + "channel %s has the incorrect value %d (must be between 0 and 255)" + (Channel.name channel) + (Channel.value channel)) +;; + +(** [to_ansi color] produces an SGR escape portion that can be + embedded in a string based on the [color]. *) +let to_ansi ~(ground : Ground.t) : t -> string = function + | Minimal { color; bright } -> + Printf.sprintf "%d%dm" (Ground.to_int ~bright ground) (Minimal.to_int color) + | Advanced color -> + Printf.sprintf "%d8;5;%dm" (Ground.to_int ground) (Int8.to_int color) + | Rgb (r, g, b) -> + Printf.sprintf + "%d8;2;%d;%d;%dm" + (Ground.to_int ground) + (Int8.to_int r) + (Int8.to_int g) + (Int8.to_int b) +;; diff --git a/lib/Formatting.ml b/lib/Formatting.ml new file mode 100644 index 0000000..e88f316 --- /dev/null +++ b/lib/Formatting.ml @@ -0,0 +1,128 @@ +module Token_type = struct + type t = + | Comment + | Documentation + | Keyword + | Literal_constant + | Literal_string + | String_template + | Identifier + | Constant (* for languages with mutable identifiers by default *) + | Parameter + | Variable_type + | Variable_lifetime + | Function + | Method + | Procedure + | Function_special (* e.g. built-in functions *) + | Method_special (* e.g. Python's dunder methods *) + | Type + | Class + | Struct (* or record *) + | Trait (* or typeclass *) + | Macro + | Module (* or namespace *) + | Operator_expr (* e.g. `*` in `8 * 5` *) + | Operator_stmt (* e.g. `=` in `let x = 3` *) + | Operator_special + | Pair (* also called "bracket" *) + | Punctuation_strong + | Punctuation_weak + | Space + | Indent + | Line_break +end + +module Stylizer = struct + type t = Token_type.t -> Styling.t + + let default : t = + let open Token_type in + function + | Comment | Documentation -> Styling.create ~dim:true () + | Keyword | Punctuation_strong -> + Styling.create ~foreground:Color.magenta ~bold:true () + | Operator_stmt | Operator_special -> Styling.create ~foreground:Color.magenta () + | Identifier -> Styling.create ~foreground:Color.cyan () + | Parameter -> Styling.create ~foreground:Color.cyan ~italic:true () + | Constant | Literal_constant | Function_special -> + Styling.create ~foreground:(Color.make_rgb_exn 153 51 204) () + | Function | Method | Method_special | Procedure | Operator_expr -> + Styling.create ~foreground:Color.blue () + | Variable_type | Variable_lifetime | Type | Class | Trait | Struct | Module -> + Styling.create ~foreground:Color.yellow () + | Macro -> Styling.create ~foreground:Color.red () + | Literal_string | String_template -> Styling.create ~foreground:Color.green () + | Pair | Punctuation_weak | Space | Indent | Line_break -> Styling.none + ;; +end + +module Token = struct + (** [Token] is the formatter token data type. *) + + (** A token is simply a pair (token type, lexeme). *) + type t = Token_type.t * string + + (** Token representing a single whitespace. + + Exists as a constant due to its recurrent use. *) + let space : t = Token_type.Space, " " + + (** Token representing a line break. + It is useful if you want to have a potential newline if + it does not fit in one line. + + Exists as a constant due to its recurrent use. *) + let line_break : t = Token_type.Line_break, "\n" + + (** Token representing a comma. + + Exists as a constant due to its recurrent use. *) + let comma : t = Token_type.Punctuation_weak, "," + + (** Token representing a colon. + + Exists as a constant due to its recurrent use. *) + let colon : t = Token_type.Punctuation_strong, ":" + + let format ?(stylizer : Stylizer.t = Stylizer.default) : t -> string = + fun (token_type, lexeme) -> + Printf.sprintf + "%s%s\x1b[22;23;24;25;39;49m" + (Styling.to_ansi (stylizer token_type)) + lexeme + ;; +end + +module type TOKENIZABLE = sig + (** [TOKENIZABLE] is the interface for languages which terms + can be transformed into a stream of formatter tokens. *) + + (** [t] encodes the language. *) + type t + + (** [tokenize term] transforms [term] into a stream of + formatter tokens. *) + val tokenize : t -> Token.t list +end + +(** [tokenize value ~using:(module M)] transforms [value] to a + list of tokens. *) +let tokenize : type t. t -> using:(module TOKENIZABLE with type t = t) -> Token.t list = + fun value ~using:(module M) -> M.tokenize value +;; + +(** [format ?stylizer value ~using:(module M)] transforms the + [value] into a pretty-printable string using the [stylizer] + if [M] provides tokenization for the [value] type. *) +let format + : type t. + ?stylizer:Stylizer.t -> t -> using:(module TOKENIZABLE with type t = t) -> string + = + fun ?(stylizer = Stylizer.default) value ~using:(module M) -> + (* TODO: handle line breaks / width-aware formatting *) + value + |> tokenize ~using:(module M) + |> List.map (Token.format ~stylizer) + |> String.concat "" +;; diff --git a/lib/Styling.ml b/lib/Styling.ml new file mode 100644 index 0000000..ea6c9d1 --- /dev/null +++ b/lib/Styling.ml @@ -0,0 +1,59 @@ +(** [Styling] encodes terminal styling as a CSS-like language. *) + +type t = + { foreground : Color.t option + ; background : Color.t option + ; bold : bool + ; dim : bool + ; italic : bool + ; underlined : bool + } + +let none : t = + { foreground = None + ; background = None + ; bold = false + ; dim = false + ; italic = false + ; underlined = false + } +;; + +let create + ?(foreground : Color.t option) + ?(background : Color.t option) + ?(bold : bool = false) + ?(dim : bool = false) + ?(italic : bool = false) + ?(underlined : bool = false) + () + : t + = + { foreground; background; bold; dim; italic; underlined } +;; + +let make_sgr_sequence (inner : string) : string = "\x1b[" ^ inner ^ "m" + +let add_color_to_buffer + (buffer : Buffer.t) + (color : Color.t option) + ~(ground : Color.Ground.t) + : unit + = + match color with + | None -> () + | Some color' -> + Buffer.add_string buffer (make_sgr_sequence (Color.to_ansi ~ground color')) +;; + +let to_ansi : t -> string = function + | { foreground; background; bold; dim; italic; underlined } -> + let buffer = Buffer.create 16 in + if bold then Buffer.add_string buffer (make_sgr_sequence "1"); + if dim then Buffer.add_string buffer (make_sgr_sequence "2"); + if italic then Buffer.add_string buffer (make_sgr_sequence "3"); + if underlined then Buffer.add_string buffer (make_sgr_sequence "4"); + add_color_to_buffer buffer foreground ~ground:Color.foreground; + add_color_to_buffer buffer background ~ground:Color.background; + Buffer.contents buffer +;; diff --git a/lib/Styling.mli b/lib/Styling.mli new file mode 100644 index 0000000..a887f54 --- /dev/null +++ b/lib/Styling.mli @@ -0,0 +1,27 @@ +type t = + { foreground : Color.t option + ; background : Color.t option + ; bold : bool + ; dim : bool + ; italic : bool + ; underlined : bool + } + +(** [none] is the empty styling. *) +val none : t + +(** [create ?foreground ?background ?bold ?dim ?italic ?underlined ()] + creates a new style object given the provided configuration. *) +val create + : ?foreground:Color.t + -> ?background:Color.t + -> ?bold:bool + -> ?dim:bool + -> ?italic:bool + -> ?underlined:bool + -> unit + -> t + +(** [to_ansi styling] renders the [styling] to an ANSI escape + sequence as a string. *) +val to_ansi : t -> string diff --git a/lib/Util.ml b/lib/Util.ml new file mode 100644 index 0000000..b31bac3 --- /dev/null +++ b/lib/Util.ml @@ -0,0 +1,95 @@ +module Int8 : sig + (** Represents an 8-bit integer. *) + + type t = private int + + (** [to_int value] converts [value] to a built-in integer. *) + val to_int : t -> int + + (** [of_int value] converts [value] to an 8-bit integer. + Returns [None] if [value] does not fit in 8 bits. *) + val of_int : int -> t option + + (** [of_int_exn value] converts [value] to a 8-bit integer. + Raises a [Failure] exception if [value] does not fit in 8 + bits. *) + val of_int_exn : int -> t +end = struct + type t = int + + let to_int : t -> int = fun value -> value + + let of_int (value : int) : t option = + if not (0 <= value && value < 256) then None else Some value + ;; + + let of_int_exn (value : int) : t = + match of_int value with + | None -> failwith "expected an integer between 0 and 255 (both included)" + | Some value' -> value' + ;; +end + +module Triplet = struct + type ('a, 'b, 'c) t = 'a * 'b * 'c + + (** [all triplet] returns a version of [triplet] wrapped in + [option], which is of the [Some] variant if every member + is also of the [Some variant], [None] otherwise. *) + let all : ('a option, 'b option, 'c option) t -> ('a, 'b, 'c) t option = function + | Some first, Some second, Some third -> Some (first, second, third) + | _ -> None + ;; + + (** [all_ok triplet] returns a version of [triplet] wrapped in + [result], which is of the [Ok] variant if every member + is also of the [Ok] variant, otherwise it is whichever + is the first [Error]. *) + let all_ok + : (('a, 'e) result, ('b, 'e) result, ('c, 'e) result) t -> (('a, 'b, 'c) t, 'e) result + = function + | Ok first, Ok second, Ok third -> Ok (first, second, third) + | (Error _ as error), _, _ -> error + | _, (Error _ as error), _ -> error + | _, _, (Error _ as error) -> error + ;; + + (** [all_error triplet] returns a version of [triplet] wrapped in + [result], which is of the [Error] variant if every member + is also of the [Error] variant, otherwise it is whichever + is the first [Ok]. *) + let all_error (* Somehow ocamlformat leaves two spaces after the colon *) + : (('r, 'e1) result, ('r, 'e2) result, ('r, 'e3) result) t + -> ('r, ('e1, 'e2, 'e3) t) result + = function + | Error first, Error second, Error third -> Error (first, second, third) + | (Ok _ as ok), _, _ -> ok + | _, (Ok _ as ok), _ -> ok + | _, _, (Ok _ as ok) -> ok + ;; + + (** [any_ok triplet] returns a version of [triplet] wrapped + in [result], which is the first [Ok] encountered if there + is any, otherwise it is the triplet wrapped in [Error]. + + It is the same as [all_error], but it is semantically + useful to have it as a separate function. *) + let any_ok = all_error + + (** [map func1 func2 func3 triplet] produces a new triplet + where each member is mapped to its corresponding function. + That is, if [triplet] is [(first, second, third)], the + result will be [(func1 first, func2 second, func3 third)]. *) + let map (func1 : 'a -> 'd) (func2 : 'b -> 'e) (func3 : 'c -> 'f) + : ('a, 'b, 'c) t -> ('d, 'e, 'f) t + = function + | first, second, third -> func1 first, func2 second, func3 third + ;; + + (** [map_uniform func triplet] maps [func] to every member of + the triplet. + It is equivalent to [map func func func triplet]. *) + let map_uniform ~(func : 'a -> 'b) : ('a, 'a, 'a) t -> ('b, 'b, 'b) t = + map func func func + ;; +end diff --git a/lib/dune b/lib/dune new file mode 100644 index 0000000..658b59b --- /dev/null +++ b/lib/dune @@ -0,0 +1,3 @@ +(library + (public_name ansifmt) + (name ansifmt)) diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..f0c25c8 --- /dev/null +++ b/test/dune @@ -0,0 +1,2 @@ +(test + (name test_ansifmt)) diff --git a/test/test_ansifmt.ml b/test/test_ansifmt.ml new file mode 100644 index 0000000..4e9c76a --- /dev/null +++ b/test/test_ansifmt.ml @@ -0,0 +1 @@ +(* TODO: test *)