diff --git a/doc-template.html b/doc-template.html index 7eae93a..31785b9 100644 --- a/doc-template.html +++ b/doc-template.html @@ -2,6 +2,12 @@ @@ -65,5 +103,6 @@

Table of Contents

+ diff --git a/docs/index.html b/docs/index.html index 22f17d2..ec892be 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,6 +2,12 @@ @@ -217,279 +255,434 @@

cyan.cli

cyan.colorstring

Some wrappers/conveniences around working with ansi escape codes. For example getting the length of a string that contains escape codes shouldnt include them

-

ColorString:copy(): ColorString -

-

Make a deep copy of a ColorString

-

ColorString:len(): integer -

-

Get the length of a given ColorString, not counting any escape sequences.

Note that ColorString:len() >= ColorString:tostring():len()

-

ColorString:surround(col: {integer}) +

method ColorString:copy

-

surrounds a string with a color

-

ColorString:to_raw(): string +
function ColorString:copy(): ColorString

Make a deep copy of a ColorString

+

method ColorString:len

-

Converts a Colorstring to a regular string, stripping any ANSI escapes

-

ColorString:tostring(): string +
function ColorString:len(): integer

Get the length of a given ColorString, not counting any escape sequences.

Note that ColorString:len() >= ColorString:tostring():len()

+

method ColorString:surround

-

Converts a Colorstring to a regular string with the correct ANSI escapes

-

type ColorString +
function ColorString:surround(
+	col: {integer}
+)

surrounds a string with a color

+

method ColorString:to_raw

-
record ColorString
content: {string | {integer}}
len: function(ColorString): integer
tostring: function(ColorString): string

metamethod __len: function(ColorString): integer
metamethod __concat: function(ColorString | string, ColorString | string): ColorString
end
-

The main object that this library consumes and produces. It basically implements the 'string' interface and can be used wherever a string is.

Colors are described as arrays of numbers that directly correspond to ANSI escape sequences

-

colorstring.copy(str: string | ColorString): string | ColorString +
function ColorString:to_raw(): string

Converts a Colorstring to a regular string, stripping any ANSI escapes

+

method ColorString:tostring

-

If input is a colorstring, return a deep copy of it, otherwise just return the input

-

colorstring.highlight(hl: {integer}, str: string): ColorString +
function ColorString:tostring(): string

Converts a Colorstring to a regular string with the correct ANSI escapes

+

type ColorString

-

Create a Colorstring by surrounding a string with the given ANSI color and an ANSI reset

-

colorstring.new(...: string | {integer}): ColorString -

-

The Colorstring constructor

-

colorstring.rgb_bg(r: integer, g: integer, b: integer): {integer, integer, integer, integer, integer} -

-

The ansi escape for an arbitrary RGB background color

-

colorstring.rgb_fg(r: integer, g: integer, b: integer): {integer, integer, integer, integer, integer} -

-

The ansi escape for an arbitrary RGB foreground color

-

cyan.command +
record ColorString
+	content: {string | {integer}}
+	len: function(ColorString): integer
+	tostring: function(ColorString): string
+
+	metamethod __len: function(ColorString): integer
+	metamethod __concat: function(ColorString | string, ColorString | string): ColorString
+end

The main object that this library consumes and produces. It basically implements the 'string' interface and can be used wherever a string is.

Colors are described as arrays of numbers that directly correspond to ANSI escape sequences

+

function colorstring.copy +

+
function colorstring.copy(
+	str: string | ColorString
+): string | ColorString

If input is a colorstring, return a deep copy of it, otherwise just return the input

+

function colorstring.highlight +

+
function colorstring.highlight(
+	hl: {integer},
+	str: string
+): ColorString

Create a Colorstring by surrounding a string with the given ANSI color and an ANSI reset

+

function colorstring.new +

+
function colorstring.new(
+	...: string | {integer}
+): ColorString

The Colorstring constructor

+

function colorstring.rgb_bg +

+
function colorstring.rgb_bg(
+	r: integer,
+	g: integer,
+	b: integer
+): {integer, integer, integer, integer, integer}

The ansi escape for an arbitrary RGB background color

+

function colorstring.rgb_fg +

+
function colorstring.rgb_fg(
+	r: integer,
+	g: integer,
+	b: integer
+): {integer, integer, integer, integer, integer}

The ansi escape for an arbitrary RGB foreground color

+

cyan.command

The common interface for commands to implement

-

type Command -

-
record Command
name: string
description: string
argparse: function(argparse.Command)
script_hooks: {string}
exec: CommandFn
end
-

The interface

-

command.get(name: string): Command -

-

Get a command that was created with command.new

Works whether or not command.register_all was called

-

command.merge_args_into_config(cfg: config.Config, args: Args) -

-

Merge the relevant entries of the provided command arguments into the provided config table

-

command.new(cmd: Command) -

-

Create a new command

This is stored in an internal cache and will do nothing unless command.register_all is called afterwards

-

command.register_all(p: argparse.Parser) -

-

Install all commands created with command.new into the given parser

-

cyan.config +

type Command +

+
record Command
+	name: string
+	description: string
+	argparse: function(argparse.Command)
+	script_hooks: {string}
+	exec: CommandFn
+end

The interface

+

function command.get +

+
function command.get(
+	name: string
+): Command

Get a command that was created with command.new

Works whether or not command.register_all was called

+

function command.merge_args_into_config +

+
function command.merge_args_into_config(
+	cfg: config.Config,
+	args: Args
+)

Merge the relevant entries of the provided command arguments into the provided config table

+

function command.new +

+
function command.new(
+	cmd: Command
+)

Create a new command

This is stored in an internal cache and will do nothing unless command.register_all is called afterwards

+

function command.register_all +

+
function command.register_all(
+	p: argparse.Parser
+)

Install all commands created with command.new into the given parser

+

cyan.config

Config loading API

-

type Config -

-
record Config
loaded_from: fs.Path
build_dir: string
source_dir: string
include: {string}
exclude: {string}
global_env_def: string
include_dir: {string}
module_name: string
dont_prune: {string}
scripts: {string:{string:string|{string}}}
gen_compat: tl.CompatMode
gen_target: tl.TargetMode
disable_warnings: {tl.WarningKind}
warning_error: {tl.WarningKind}
externals: {string:any}
end
-

The config data

-

config.find(): fs.Path -

-

Find config.filename in the current or parent directories

-

config.is_config(c: any): Config, {string}, {string} -

-

Check if c conforms to the Config type and return any errors and warnings generated from checking

-

config.load(): Config, {string}, {string} -

-

Try to load tlconfig.lua in the current directory

-

cyan.fs.path +

type Config +

+
record Config
+	loaded_from: fs.Path
+	build_dir: string
+	source_dir: string
+	include: {string}
+	exclude: {string}
+	global_env_def: string
+	include_dir: {string}
+	module_name: string
+	dont_prune: {string}
+	scripts: {string:{string:string|{string}}}
+	gen_compat: tl.CompatMode
+	gen_target: tl.TargetMode
+	disable_warnings: {tl.WarningKind}
+	warning_error: {tl.WarningKind}
+	externals: {string:any}
+end

The config data

+

function config.find +

+
function config.find(): fs.Path

Find config.filename in the current or parent directories

+

function config.is_config +

+
function config.is_config(
+	c: any
+): Config, {string}, {string}

Check if c conforms to the Config type and return any errors and warnings generated from checking

+

function config.load +

+
function config.load(): Config, {string}, {string}

Try to load tlconfig.lua in the current directory

+

cyan.fs.path

Object oriented lexical path management

-

Path.eq(a: Path | string, b: Path | string, use_os_sep: boolean): boolean +

function Path.eq

-

Check if two paths are equal

This function is used for the __eq metamethod with use_os_sep as false

-

Path:ancestors(): function(): Path +
function Path.eq(
+	a: Path | string,
+	b: Path | string,
+	use_os_sep: boolean
+): boolean

Check if two paths are equal

This function is used for the __eq metamethod with use_os_sep as false

+

method Path:ancestors

-

Iterate over the leading folders in a path

ex: path.new("foo/bar/baz/bat"):ancestors() will construct Path objects from "foo", "foo/bar", "foo/bar/baz"

-

Path:append(other: string | Path) +
function Path:ancestors(): function(): Path

Iterate over the leading folders in a path

ex: path.new("foo/bar/baz/bat"):ancestors() will construct Path objects from "foo", "foo/bar", "foo/bar/baz"

+

method Path:append

-

Mutate the given Path by appending another path to it

Traversals in the other path will be normalized

 local p = path.new "foo/bar"
p:append "../baz"
assert(p == path.new "foo/baz")

-

Path:copy(): Path +
function Path:append(
+	other: string | Path
+)

Mutate the given Path by appending another path to it

Traversals in the other path will be normalized

 local p = path.new "foo/bar"
p:append "../baz"
assert(p == path.new "foo/baz")

+

method Path:copy

-

Create a copy of the given path

-

Path:exists(): boolean +
function Path:copy(): Path

Create a copy of the given path

+

method Path:exists

-

Check if the path exists

-

Path:is_absolute(): boolean +
function Path:exists(): boolean

Check if the path exists

+

method Path:is_absolute

-

Returns whether the path is absolute

On windows, checks for paths like "C:\...", elsewhere looks for "/..."

-

Path:is_directory(): boolean +
function Path:is_absolute(): boolean

Returns whether the path is absolute

On windows, checks for paths like "C:\...", elsewhere looks for "/..."

+

method Path:is_directory

-

Get whether the "mode" attribute of the given path is set to "directory"

-

Path:is_file(): boolean +
function Path:is_directory(): boolean

Get whether the "mode" attribute of the given path is set to "directory"

+

method Path:is_file

-

Get whether the "mode" attribute of the given path is set to "file"

-

Path:is_in(dirname: string | Path, use_os_sep: boolean): boolean +
function Path:is_file(): boolean

Get whether the "mode" attribute of the given path is set to "file"

+

method Path:is_in

-

returns true if the path is inside the given directory

If relative and absolute paths are mixed, the relative path is assumed to be in the current working directory (as determined by lfs.currentdir())

If dirname is a string, a path will be constructed using path.new with use_os_sep

-

Path:match(patt: string): boolean +
function Path:is_in(
+	dirname: string | Path,
+	use_os_sep: boolean
+): boolean

returns true if the path is inside the given directory

If relative and absolute paths are mixed, the relative path is assumed to be in the current working directory (as determined by lfs.currentdir())

If dirname is a string, a path will be constructed using path.new with use_os_sep

+

method Path:match

-

See if the given path matches the pattern

Path separators in patterns are always represented with '/'.

* characters represent any number of non-path-separator characters

**/ represent any number of directories

-

Path:match_any(patts: {string}): integer, string +
function Path:match(
+	patt: string
+): boolean

See if the given path matches the pattern

Path separators in patterns are always represented with '/'.

* characters represent any number of non-path-separator characters

**/ represent any number of directories

+

method Path:match_any

-

See if the given path matches any of the given patterns

-

Path:mk_parent_dirs(): boolean, string +
function Path:match_any(
+	patts: {string}
+): integer, string

See if the given path matches any of the given patterns

+

method Path:mk_parent_dirs

-

Attempt to create the leading directories of a given path

-

Path:mkdir(): boolean, string +
function Path:mk_parent_dirs(): boolean, string

Attempt to create the leading directories of a given path

+

method Path:mkdir

-

Attempt to create a directory at the given path, creating the parent directories if needed. Can be seen as an equivalent to mkdir -p

-

Path:mod_time(): integer +
function Path:mkdir(): boolean, string

Attempt to create a directory at the given path, creating the parent directories if needed. Can be seen as an equivalent to mkdir -p

+

method Path:mod_time

-

Get the "modification" attribute of a file

-

Path:normalize() +
function Path:mod_time(): integer

Get the "modification" attribute of a file

+

method Path:normalize

-

Modify path in place to remove any traversals that it can

 local p = path.new "foo/bar/../baz"
p:normalize()
assert(p == path.new "foo/baz")

if a traversal can't be removed it will remain

 local p = path.new "../baz"
p:normalize()
assert(p == path.new "../baz")

-

Path:prepend(other: string | Path) +
function Path:normalize()

Modify path in place to remove any traversals that it can

 local p = path.new "foo/bar/../baz"
p:normalize()
assert(p == path.new "foo/baz")

if a traversal can't be removed it will remain

 local p = path.new "../baz"
p:normalize()
assert(p == path.new "../baz")

+

method Path:prepend

-

Mutate the given Path by prepending another path to it

-

Path:relative_to(other: Path): Path +
function Path:prepend(
+	other: string | Path
+)

Mutate the given Path by prepending another path to it

+

method Path:relative_to

-

Expresses a path in terms of another path. If any relative paths are given, they are treated as though they are in the current directory

for example: path.new("/foo/bar/baz"):relative_to(path.new("/foo/bat")) == path.new "../bar/baz"

-

Path:remove_leading(p: string | Path) +
function Path:relative_to(
+	other: Path
+): Path

Expresses a path in terms of another path. If any relative paths are given, they are treated as though they are in the current directory

for example: path.new("/foo/bar/baz"):relative_to(path.new("/foo/bat")) == path.new "../bar/baz"

+

method Path:remove_leading

-

Mutate the given path by removing the leading parts from the given path

Will error if you attempt to mix absolute and non-absolute paths

-

Path:to_absolute() +
function Path:remove_leading(
+	p: string | Path
+)

Mutate the given path by removing the leading parts from the given path

Will error if you attempt to mix absolute and non-absolute paths

+

method Path:to_absolute

-

Modify a path in place to become an absolute path

When the path is already absolute, does nothing

Otherwise, prepends the current directory

-

Path:to_real_path(): string +
function Path:to_absolute()

Modify a path in place to become an absolute path

When the path is already absolute, does nothing

Otherwise, prepends the current directory

+

method Path:to_real_path

-

Convert a Path to a string describing a real path

-

Path:tostring(): string +
function Path:to_real_path(): string

Convert a Path to a string describing a real path

+

method Path:tostring

-

Convert a path to a string. Always uses '/' as a path separator. Intended for displaying purposes. For an actual path in the filesystem, use Path:to_real_path()

Used for the __tostring metamethod

-

type Path +
function Path:tostring(): string

Convert a path to a string. Always uses '/' as a path separator. Intended for displaying purposes. For an actual path in the filesystem, use Path:to_real_path()

Used for the __tostring metamethod

+

type Path

-
record Path
{string}

metamethod __concat: function(Path | string, Path | string): Path
metamethod __eq: function(Path | string, Path | string): boolean
end
-

The main path object. Basically just an array of strings with some methods and metamethods to interact with other paths

-

path.ensure(s: string | Path, use_os_sep: boolean): Path -

-

Ensures s is a Path.

If s is a string, parse it into a path otherwise return s unmodified

-

path.new(s: string, use_os_sep: boolean): Path -

-

The Path constructor

By default uses '/' as a path separator

-

cyan.fs +
record Path
+	{string}
+
+	metamethod __concat: function(Path | string, Path | string): Path
+	metamethod __eq: function(Path | string, Path | string): boolean
+end

The main path object. Basically just an array of strings with some methods and metamethods to interact with other paths

+

function path.ensure +

+
function path.ensure(
+	s: string | Path,
+	use_os_sep: boolean
+): Path

Ensures s is a Path.

If s is a string, parse it into a path otherwise return s unmodified

+

function path.new +

+
function path.new(
+	s: string,
+	use_os_sep: boolean
+): Path

The Path constructor

By default uses '/' as a path separator

+

cyan.fs

Filesystem and path management

-

fs.chdir(p: string | Path): boolean, string -

-

Change the current directory to p

-

fs.copy(source: string | Path, dest: string | Path): boolean, string -

-

Copy a file

uses fs.read internally to get and cache the contents of source

-

fs.cwd(): Path -

-

Get the current working directory as an fs.Path

-

fs.dir(dir: string | Path, include_dotfiles: boolean): function(): Path -

-

Iterate over the given directory, returning fs.Path objects

By default, will not include paths that start with '.'

-

fs.extension_split(p: Path | string, ndots: integer): string, string -

-

Split a path on its extension. Forces the extension to be lowercase.

the ndots argument lets you specify the upper limit of how many dots the extension can have

ex:

 fs.extension_split("foo.d.tl") => "foo.d", ".tl"
fs.extension_split("foo.d.tl", 2) => "foo", ".d.tl"
fs.extension_split("foo.D.TL", 2) => "foo", ".d.tl"

-

fs.get_line(p: string, n: integer): string, string -

-

Gets line n of a file.

if the file is less than n lines, returns nil

if there was an error opening the file, returns nil, err

Uses fs.read() internally, which caches reads

-

fs.path_concat(a: string, b: string): string -

-

Concatenate two strings using the os path separator

-

fs.read(p: string): string, string -

-

Open a file, read it, close the file, return the contents or nil and an error if it couldn't be opened

Additionally caches results so multiple locations can read the same file for minimal cost. There is currently no way to clear out this cache.

-

fs.scan_dir(dir: string | Path, include: {string}, exclude: {string}, include_directories: boolean): function(): Path -

-

Recursively iterate over the files in a directory, following the provided include and exclude patterns

For information on path patterns, see the Path:match() method

-

fs.search_parent_dirs(start_path: string | Path, fname: string): Path -

-

Search for a file in the parent directories of the given path. Returns the path of the file found.

e.g. if file.txt is in /foo/bar, then fs.search_parent_dirs("/foo/bar/baz", "file.txt") == path.new "/foo/bar/file.txt"

-

cyan.graph +

function fs.chdir +

+
function fs.chdir(
+	p: string | Path
+): boolean, string

Change the current directory to p

+

function fs.copy +

+
function fs.copy(
+	source: string | Path,
+	dest: string | Path
+): boolean, string

Copy a file

uses fs.read internally to get and cache the contents of source

+

function fs.cwd +

+
function fs.cwd(): Path

Get the current working directory as an fs.Path

+

function fs.dir +

+
function fs.dir(
+	dir: string | Path,
+	include_dotfiles: boolean
+): function(): Path

Iterate over the given directory, returning fs.Path objects

By default, will not include paths that start with '.'

+

function fs.extension_split +

+
function fs.extension_split(
+	p: Path | string,
+	ndots: integer
+): string, string

Split a path on its extension. Forces the extension to be lowercase.

the ndots argument lets you specify the upper limit of how many dots the extension can have

ex:

 fs.extension_split("foo.d.tl") => "foo.d", ".tl"
fs.extension_split("foo.d.tl", 2) => "foo", ".d.tl"
fs.extension_split("foo.D.TL", 2) => "foo", ".d.tl"

+

function fs.get_line +

+
function fs.get_line(
+	p: string,
+	n: integer
+): string, string

Gets line n of a file.

if the file is less than n lines, returns nil

if there was an error opening the file, returns nil, err

Uses fs.read() internally, which caches reads

+

function fs.path_concat +

+
function fs.path_concat(
+	a: string,
+	b: string
+): string

Concatenate two strings using the os path separator

+

function fs.read +

+
function fs.read(
+	p: string
+): string, string

Open a file, read it, close the file, return the contents or nil and an error if it couldn't be opened

Additionally caches results so multiple locations can read the same file for minimal cost. There is currently no way to clear out this cache.

+

function fs.scan_dir +

+
function fs.scan_dir(
+	dir: string | Path,
+	include: {string},
+	exclude: {string},
+	include_directories: boolean
+): function(): Path

Recursively iterate over the files in a directory, following the provided include and exclude patterns

For information on path patterns, see the Path:match() method

+

function fs.search_parent_dirs +

+
function fs.search_parent_dirs(
+	start_path: string | Path,
+	fname: string
+): Path

Search for a file in the parent directories of the given path. Returns the path of the file found.

e.g. if file.txt is in /foo/bar, then fs.search_parent_dirs("/foo/bar/baz", "file.txt") == path.new "/foo/bar/file.txt"

+

cyan.graph

A utility for building directed acyclic graphs of Teal source files

This is the main driver behind the build command

-

Dag:find(fstr: string | fs.Path): Node -

-

Find a node in the graph with the given path name

-

Dag:insert_file(fstr: string | fs.Path, in_dir: string | fs.Path): boolean, {string} -

-

Inserts a file and its dependencies into a graph

Ignores absolute paths and non .tl or .lua files

If in_dir is provided, dependencies of the given file will not be added to the graph unless they are inside of the given dir

Returns false if inserting the file introduced a circular dependency along with a list of the filenames in the cycle

-

Dag:mark_each(predicate: function(fs.Path): boolean) -

-

For each node in the graph, if predicate returns true for that input path, the node is marked for compilation, and that node's children are marked for type checking

-

Dag:marked_nodes(): function(): Node -

-

Iterate over every node that has been marked, no matter what the mark is

-

Dag:nodes(): function(): Node -

-

Iterate over nodes in order of dependents

If two nodes have the same number of dependent nodes, the order of iteration between those two nodes is not guaranteed

-

type Dag -

-
record Dag
-- private fields
_nodes_by_filename: {string:Node}

end
-

The graph object

-

type Node -

-
record Node
input: fs.Path
output: fs.Path
modules: {string:fs.Path}
mark: Mark
dependents: {Node:boolean}
end
-

The nodes that are stored in the graph

-

graph.empty(): Dag -

-

Initializes an empty graph

-

graph.scan_dir(dir: string | fs.Path, include: {string}, exclude: {string}): Dag, {string} -

-

Recursively scan a directory (using fs.scan_dir) and build up a graph, respecting the given include and exclude patterns

Returns nil if a circular dependency was found, along with a list of the filenames in the cycle

-

cyan.interaction +

method Dag:find +

+
function Dag:find(
+	fstr: string | fs.Path
+): Node

Find a node in the graph with the given path name

+

method Dag:insert_file +

+
function Dag:insert_file(
+	fstr: string | fs.Path,
+	in_dir: string | fs.Path
+): boolean, {string}

Inserts a file and its dependencies into a graph

Ignores absolute paths and non .tl or .lua files

If in_dir is provided, dependencies of the given file will not be added to the graph unless they are inside of the given dir

Returns false if inserting the file introduced a circular dependency along with a list of the filenames in the cycle

+

method Dag:mark_each +

+
function Dag:mark_each(
+	predicate: function(fs.Path): boolean
+)

For each node in the graph, if predicate returns true for that input path, the node is marked for compilation, and that node's children are marked for type checking

+

method Dag:marked_nodes +

+
function Dag:marked_nodes(): function(): Node

Iterate over every node that has been marked, no matter what the mark is

+

method Dag:nodes +

+
function Dag:nodes(): function(): Node

Iterate over nodes in order of dependents

If two nodes have the same number of dependent nodes, the order of iteration between those two nodes is not guaranteed

+

type Dag +

+
record Dag
+
-- private fields (click to show) + _nodes_by_filename: {string:Node}
+end

The graph object

+

type Node +

+
record Node
+	input: fs.Path
+	output: fs.Path
+	modules: {string:fs.Path}
+	mark: Mark
+	dependents: {Node:boolean}
+end

The nodes that are stored in the graph

+

function graph.empty +

+
function graph.empty(): Dag

Initializes an empty graph

+

function graph.scan_dir +

+
function graph.scan_dir(
+	dir: string | fs.Path,
+	include: {string},
+	exclude: {string}
+): Dag, {string}

Recursively scan a directory (using fs.scan_dir) and build up a graph, respecting the given include and exclude patterns

Returns nil if a circular dependency was found, along with a list of the filenames in the cycle

+

cyan.interaction

Module for handling when input from the user is needed

-

interaction.yes_no_prompt( - prompt: string | cs.ColorString, - logger: log.Logger, - default: boolean, - affirm: {string}, - deny: {string} -): boolean -

-

Ask the user to affirm or deny a given prompt. The user input will be compared against the given affirm and deny lists (case-insensitive), with defaults used if they are not provided.

The given logger will be used to print the prompt, and log.info if none is provided.

-

cyan.log +

function interaction.yes_no_prompt +

+
function interaction.yes_no_prompt(
+	prompt: string | cs.ColorString,
+	logger: log.Logger,
+	default: boolean,
+	affirm: {string},
+	deny: {string}
+): boolean

Ask the user to affirm or deny a given prompt. The user input will be compared against the given affirm and deny lists (case-insensitive), with defaults used if they are not provided.

The given logger will be used to print the prompt, and log.info if none is provided.

+

cyan.log

Console logging utils, not to be confused with log files

Each logger object has the same __call signature of function(...: any), and by default the following are provided:

Name Stream Description
info stdout General info, should be seen as the default, silenced by --quiet
extra stdout Extra info that isn't strictly necessary, enabled via -v extra, silenced by --quiet
warn stderr Used to display warnings, silenced by --quiet
err stderr Used to display errors
debug stderr Used for debugging, uses the inspect module (if it is found) to print its arguments, enabled by -v debug

You may notice that these are nicely padded and after the first line the prefix is replaced by a '...'. Another function is provided, create_logger,

 create_logger: function(
stream: FILE,
verbosity_threshold: Verbosity,
prefix: string | ColorString,
cont: string | ColorString,
inspector: function(any): string
): Logger

to automatically generate formatted output. cont defaults to "..." and inspector defaults to tostring. Prefixes will be padded to 10 characters wide by default, so your logging may look off from the default if your prefix is longer.

Additionally, loggers will try to detect whether or not to display colors. This is only handled with the ColorString type to avoid the many pitfalls of trying to parse ANSI escape sequences. If a regular string contains any escape sequences or an inspector produces them (outside of a ColorString) it will not be handled.

-

Logger:cont(...: any) -

-

Log only using the continuation prefix.

-

Logger:cont_nonl(...: any) -

-

Log only using the continuation prefix, but don't put a newline at the end.

-

Logger:copy( - new_prefix: string | cs.ColorString, - new_continuation: string | cs.ColorString -): Logger -

-

Create a copy of a logger, deep copying relevant data

-

Logger:format(fmt: string, ...: any) -

-

Call string.format with the given arguments and log that.

-

Logger:format_nonl(fmt: string, ...: any) -

-

Call string.format with the given arguments and log that, without a new line.

-

Logger:nonl(...: any) -

-

Same as calling the logger, but don't put a newline at the end

-

Logger:should_log(): boolean -

-

Returns whether the current verbosity is less than or equal to this loggers verbosity threshold.

-

type Logger -

-
record Logger
stream: FILE
verbosity_threshold: Verbosity
prefix: string | cs.ColorString
continuation: string | cs.ColorString
inspector: function(any): string

metamethod __call: function(...: any)
end
-

The data needed for a logger to do its job.

-

type Verbosity -

-
enum Verbosity
"quiet"
"normal"
"extra"
"debug"
end
-

The thresholds for loggers to actually write their output

-

create_logger( - stream: FILE, - verbosity_threshold: Verbosity, - prefix: string | cs.ColorString, - cont: string | cs.ColorString, - inspector: function(any): string -): Logger -

-

Creates a Logger as described above

-

log.set_prefix_padding(padding: integer) -

-

Globally set the padding of the prefixes of loggers.

-

log.set_verbosity(level: Verbosity) -

-

Globally set the verbosity of the logging module.

-

cyan.meta +

method Logger:cont +

+
function Logger:cont(
+	...: any
+)

Log only using the continuation prefix.

+

method Logger:cont_nonl +

+
function Logger:cont_nonl(
+	...: any
+)

Log only using the continuation prefix, but don't put a newline at the end.

+

method Logger:copy +

+
function Logger:copy(
+	new_prefix: string | cs.ColorString,
+	new_continuation: string | cs.ColorString
+): Logger

Create a copy of a logger, deep copying relevant data

+

method Logger:format +

+
function Logger:format(
+	fmt: string,
+	...: any
+)

Call string.format with the given arguments and log that.

+

method Logger:format_nonl +

+
function Logger:format_nonl(
+	fmt: string,
+	...: any
+)

Call string.format with the given arguments and log that, without a new line.

+

method Logger:nonl +

+
function Logger:nonl(
+	...: any
+)

Same as calling the logger, but don't put a newline at the end

+

method Logger:should_log +

+
function Logger:should_log(): boolean

Returns whether the current verbosity is less than or equal to this loggers verbosity threshold.

+

type Logger +

+
record Logger
+	stream: FILE
+	verbosity_threshold: Verbosity
+	prefix: string | cs.ColorString
+	continuation: string | cs.ColorString
+	inspector: function(any): string
+
+	metamethod __call: function(...: any)
+end

The data needed for a logger to do its job.

+

type Verbosity +

+
enum Verbosity
+	"quiet"
+	"normal"
+	"extra"
+	"debug"
+end

The thresholds for loggers to actually write their output

+

function create_logger +

+
function create_logger(
+	stream: FILE,
+	verbosity_threshold: Verbosity,
+	prefix: string | cs.ColorString,
+	cont: string | cs.ColorString,
+	inspector: function(any): string
+): Logger

Creates a Logger as described above

+

function log.set_prefix_padding +

+
function log.set_prefix_padding(
+	padding: integer
+)

Globally set the padding of the prefixes of loggers.

+

function log.set_verbosity +

+
function log.set_verbosity(
+	level: Verbosity
+)

Globally set the verbosity of the logging module.

+

cyan.meta

Meta information about Cyan itself

cyan.sandbox @@ -498,127 +691,236 @@

cyan.sandbox

cyan.script

The script loading api

-

script.disable() -

-

Make everything in this library a no-op, there is currently no way to re-enable this

-

script.emit_hook(name: string, ...: any): boolean, string -

-

Iterates through each loaded script and runs any with the given hook, logging each script that it ran, and stopping early if any error

-

script.emitter(name: string, ...: any): function(): fs.Path, boolean, string -

-

Emit a hook to load and run all registered scripts that run on the given hook.

This function will assert that ensure_loaded_for_command was called before.

Returns an iterator that will run the next script when called and returns the path to the script, whether the script was loaded and ran with no errors, and an error message if it didn't

-

script.ensure_loaded_for_command(name: string): boolean, string | tl.Result -

-

Attempts to load each script that the given command may need

-

script.register(path: string, command_name: string, hooks: string | {string}) -

-

Registers a file path as a lua/teal script to execute for the given hook(s) when script.emit_hook is called

This is called by the cli driver to register the scripts found in the config file with the relevant hooks

Note: this function does not attempt to actually load the file. Scripts are loaded all at once via ensure_loaded_for_command

-

cyan.tlcommon +

function script.disable +

+
function script.disable()

Make everything in this library a no-op, there is currently no way to re-enable this

+

function script.emit_hook +

+
function script.emit_hook(
+	name: string,
+	...: any
+): boolean, string

Iterates through each loaded script and runs any with the given hook, logging each script that it ran, and stopping early if any error

+

function script.emitter +

+
function script.emitter(
+	name: string,
+	...: any
+): function(): fs.Path, boolean, string

Emit a hook to load and run all registered scripts that run on the given hook.

This function will assert that ensure_loaded_for_command was called before.

Returns an iterator that will run the next script when called and returns the path to the script, whether the script was loaded and ran with no errors, and an error message if it didn't

+

function script.ensure_loaded_for_command +

+
function script.ensure_loaded_for_command(
+	name: string
+): boolean, string | tl.Result

Attempts to load each script that the given command may need

+

function script.register +

+
function script.register(
+	path: string,
+	command_name: string,
+	hooks: string | {string}
+)

Registers a file path as a lua/teal script to execute for the given hook(s) when script.emit_hook is called

This is called by the cli driver to register the scripts found in the config file with the relevant hooks

Note: this function does not attempt to actually load the file. Scripts are loaded all at once via ensure_loaded_for_command

+

cyan.tlcommon

Common things needed by most commands in addition to wrappers around the tl api, since it isn't super stable

-

type ParseResult -

-
record ParseResult
tks: {Token}
ast: Node
reqs: {string}
errs: {tl.Error}
end
-

The result from parsing source code including the tokens, ast, calls to require, and errors

-

common.init_env_from_config(cfg: config.Config): tl.Env, string -

-

Initialize a strict Teal environment, using the relevant entries of the config to modify that environment

may return nil and an error message if something could not be applied to the environment

-

common.init_teal_env(gen_compat: boolean | tl.CompatMode, gen_target: tl.TargetMode, env_def: string): tl.Env, string -

-

Initialize a strict Teal environment

-

common.lex_file(path: string): {Token}, {tl.Error}, string -

-

reads a file, calls tl.lex on its contents, caches and returns the results

-

common.make_error_header(file: string, num_errors: integer, category: string): cs.ColorString -

-

Creates a nicely colored header to log errors

For example make_error_header("foo.tl", 10, "foo error") would produce something like 10 foo errors in foo.tl with 10 and foo.tl highlighted

-

common.parse_file(path: string): ParseResult, string -

-

calls lex_file, parses the token stream, caches and returns the results

-

common.prepend_to_lua_path(path_str: string) -

-

Prepend the given string to package.path and package.cpath.

Correctly adds ?.lua and ?/init.lua to the path

-

common.report_config_errors(errs: {string}, warnings: {string}): boolean -

-

use log.warn and log.err to report errors and warnings from config.load

-

common.report_env_results(env: tl.Env, cfg: config.Config): boolean -

-

Report all errors from a tl.Env

Returns false when errors were reported

-

common.report_errors(logger: log.Logger, errs: {tl.Error}, file: string, category: string) -

-

Logs an array of errors with nice colors and a header generated by make_error_header

-

common.report_result(r: tl.Result, c: config.Config): boolean -

-

Logs all the syntax errors, warnings, type errors, etc. from a tl.Result with proper colors

Returns false if there were any errors. This includs warnings that were promoted to errors and doesn't include warnings that were not promoted to errors.

-

common.result_has_errors(r: tl.Result, c: config.Config): boolean -

-

Returns whether or not the result has errors. Doesn't print/log anything

-

common.search_module(name: string, search_dtl: boolean): fs.Path -

-

A wrapper around tl.search_module but, returns an fs.Path and will cache results

-

common.syntax_highlight(s: string): cs.ColorString -

-

Takes Teal or Lua code and returns a ColorString highlighting things like keywords, operators, and more.

-

common.type_check_ast(ast: Node, opts: tl.TypeCheckOptions): tl.Result, string -

-

Just type checks an ast

-

cyan.util +

type ParseResult +

+
record ParseResult
+	tks: {Token}
+	ast: Node
+	reqs: {string}
+	errs: {tl.Error}
+end

The result from parsing source code including the tokens, ast, calls to require, and errors

+

function common.init_env_from_config +

+
function common.init_env_from_config(
+	cfg: config.Config
+): tl.Env, string

Initialize a strict Teal environment, using the relevant entries of the config to modify that environment

may return nil and an error message if something could not be applied to the environment

+

function common.init_teal_env +

+
function common.init_teal_env(
+	gen_compat: boolean | tl.CompatMode,
+	gen_target: tl.TargetMode,
+	env_def: string
+): tl.Env, string

Initialize a strict Teal environment

+

function common.lex_file +

+
function common.lex_file(
+	path: string
+): {Token}, {tl.Error}, string

reads a file, calls tl.lex on its contents, caches and returns the results

+

function common.make_error_header +

+
function common.make_error_header(
+	file: string,
+	num_errors: integer,
+	category: string
+): cs.ColorString

Creates a nicely colored header to log errors

For example make_error_header("foo.tl", 10, "foo error") would produce something like 10 foo errors in foo.tl with 10 and foo.tl highlighted

+

function common.parse_file +

+
function common.parse_file(
+	path: string
+): ParseResult, string

calls lex_file, parses the token stream, caches and returns the results

+

function common.prepend_to_lua_path +

+
function common.prepend_to_lua_path(
+	path_str: string
+)

Prepend the given string to package.path and package.cpath.

Correctly adds ?.lua and ?/init.lua to the path

+

function common.report_config_errors +

+
function common.report_config_errors(
+	errs: {string},
+	warnings: {string}
+): boolean

use log.warn and log.err to report errors and warnings from config.load

+

function common.report_env_results +

+
function common.report_env_results(
+	env: tl.Env,
+	cfg: config.Config
+): boolean

Report all errors from a tl.Env

Returns false when errors were reported

+

function common.report_errors +

+
function common.report_errors(
+	logger: log.Logger,
+	errs: {tl.Error},
+	file: string,
+	category: string
+)

Logs an array of errors with nice colors and a header generated by make_error_header

+

function common.report_result +

+
function common.report_result(
+	r: tl.Result,
+	c: config.Config
+): boolean

Logs all the syntax errors, warnings, type errors, etc. from a tl.Result with proper colors

Returns false if there were any errors. This includs warnings that were promoted to errors and doesn't include warnings that were not promoted to errors.

+

function common.result_has_errors +

+
function common.result_has_errors(
+	r: tl.Result,
+	c: config.Config
+): boolean

Returns whether or not the result has errors. Doesn't print/log anything

+

function common.search_module +

+
function common.search_module(
+	name: string,
+	search_dtl: boolean
+): fs.Path

A wrapper around tl.search_module but, returns an fs.Path and will cache results

+

function common.syntax_highlight +

+
function common.syntax_highlight(
+	s: string
+): cs.ColorString

Takes Teal or Lua code and returns a ColorString highlighting things like keywords, operators, and more.

+

function common.type_check_ast +

+
function common.type_check_ast(
+	ast: Node,
+	opts: tl.TypeCheckOptions
+): tl.Result, string

Just type checks an ast

+

cyan.util

Basically some extensions of the std lib. Currently these lean towards a more functional style

This is split into two main modules, str and tab. For string and table utilities respectively.

-

peek<Value>(iter: function(...: any): (Value), ...: any): function(): Value, Value -

-

Takes an iterator and turns it into an iterator that returns pairs of values

For example if some iterator f returns the sequence 1, 2, 3 peek(f) would return the pairs (1, 2), (2, 3), (3, nil)

-

str.esc(s: string, sub: string | function(string): string | {string:string}): string, integer -

-

escape any special characters in a string

use sub to control how the characters are substituted, by default a special character x will be replaced with %x

returns the new string and the number of characters replaced

-

str.pad_left(s: string, n: integer): string -

-

Prefix s with spaces so the resulting string is at least n characters long

-

str.split(s: string, del: string, no_patt: boolean): function(): string -

-

Split a string by del, returning the substring that was matched

Will error if the delimiter matches the empty string

-

str.split_find(s: string, del: string, no_patt: boolean): function(): integer, integer -

-

Split a string by del, returning the indexes of the match

Will error if the delimiter matches the empty string

-

tab.contains<Value>(t: {Value}, val: Value): boolean -

-

Report if an array contains an element (as determined by the == operator)

-

tab.ensure_scalar_array<Value>(source: Value | {Value}): {Value} -

-

If source is not an array, create an array with source as its only element

Note: Due to teal's current generic limitations, this only works if Value is a NON-table type

-

tab.filter<Value>(t: {Value}, pred: function(Value): boolean): {Value}, {Value} -

-

Create two new lists from t: the values that return true from pred and the values that return false

-

tab.from<Value>(fn: function(...: any): (Value), ...: any): {Value} -

-

Collect all the values of an iterator in a list

-

tab.intersperse<Value>(t: {Value}, val: Value): {Value} -

-

produce a new list by inserting val after each element

-

tab.ivalues<Value>(t: {any:Value}): function(): Value -

-

Iterate over the integer indexed values of a map

-

tab.keys<Key>(t: {Key:any}): function(): Key -

-

Iterate over the keys of a map

-

tab.map<Key, Value, MappedValue>(t: {Key:Value}, fn: function(Value): MappedValue): {Key:MappedValue} -

-

Create a new map from t by passing each value through fn

-

tab.map_ipairs<Value, MappedValue>(t: {Value}, fn: function(Value): MappedValue): function(): integer, MappedValue -

-

iterate over a list like ipairs does, but filter the values through fn

-

tab.merge_list<Value>(a: {Value}, b: {Value}): {Value} -

-

Create a new list by shallow copying the contents of a and b

-

tab.set<Value>(lst: {Value}): {Value:boolean} -

-

Create a Set from a list

-

tab.sort_in_place<Value>(t: {Value}, fn: function(Value, Value): boolean): {Value} -

-

Sort a table (in place) and return that table

-

tab.values<Key, Value>(t: {Key:Value}): function(): Value -

-

Iterate over the values of a map

+

function peek +

+
function peek<Value>(
+	iter: function(...: any): (Value),
+	...: any
+): function(): Value, Value

Takes an iterator and turns it into an iterator that returns pairs of values

For example if some iterator f returns the sequence 1, 2, 3 peek(f) would return the pairs (1, 2), (2, 3), (3, nil)

+

function str.esc +

+
function str.esc(
+	s: string,
+	sub: string | function(string): string | {string:string}
+): string, integer

escape any special characters in a string

use sub to control how the characters are substituted, by default a special character x will be replaced with %x

returns the new string and the number of characters replaced

+

function str.pad_left +

+
function str.pad_left(
+	s: string,
+	n: integer
+): string

Prefix s with spaces so the resulting string is at least n characters long

+

function str.split +

+
function str.split(
+	s: string,
+	del: string,
+	no_patt: boolean
+): function(): string

Split a string by del, returning the substring that was matched

Will error if the delimiter matches the empty string

+

function str.split_find +

+
function str.split_find(
+	s: string,
+	del: string,
+	no_patt: boolean
+): function(): integer, integer

Split a string by del, returning the indexes of the match

Will error if the delimiter matches the empty string

+

function tab.contains +

+
function tab.contains<Value>(
+	t: {Value},
+	val: Value
+): boolean

Report if an array contains an element (as determined by the == operator)

+

function tab.ensure_scalar_array +

+
function tab.ensure_scalar_array<Value>(
+	source: Value | {Value}
+): {Value}

If source is not an array, create an array with source as its only element

Note: Due to teal's current generic limitations, this only works if Value is a NON-table type

+

function tab.filter +

+
function tab.filter<Value>(
+	t: {Value},
+	pred: function(Value): boolean
+): {Value}, {Value}

Create two new lists from t: the values that return true from pred and the values that return false

+

function tab.from +

+
function tab.from<Value>(
+	fn: function(...: any): (Value),
+	...: any
+): {Value}

Collect all the values of an iterator in a list

+

function tab.intersperse +

+
function tab.intersperse<Value>(
+	t: {Value},
+	val: Value
+): {Value}

produce a new list by inserting val after each element

+

function tab.ivalues +

+
function tab.ivalues<Value>(
+	t: {any:Value}
+): function(): Value

Iterate over the integer indexed values of a map

+

function tab.keys +

+
function tab.keys<Key>(
+	t: {Key:any}
+): function(): Key

Iterate over the keys of a map

+

function tab.map +

+
function tab.map<Key, Value, MappedValue>(
+	t: {Key:Value},
+	fn: function(Value): MappedValue
+): {Key:MappedValue}

Create a new map from t by passing each value through fn

+

function tab.map_ipairs +

+
function tab.map_ipairs<Value, MappedValue>(
+	t: {Value},
+	fn: function(Value): MappedValue
+): function(): integer, MappedValue

iterate over a list like ipairs does, but filter the values through fn

+

function tab.merge_list +

+
function tab.merge_list<Value>(
+	a: {Value},
+	b: {Value}
+): {Value}

Create a new list by shallow copying the contents of a and b

+

function tab.set +

+
function tab.set<Value>(
+	lst: {Value}
+): {Value:boolean}

Create a Set from a list

+

function tab.sort_in_place +

+
function tab.sort_in_place<Value>(
+	t: {Value},
+	fn: function(Value, Value): boolean
+): {Value}

Sort a table (in place) and return that table

+

function tab.values +

+
function tab.values<Key, Value>(
+	t: {Key:Value}
+): function(): Value

Iterate over the values of a map

+
diff --git a/scripts/gen_documentation.tl b/scripts/gen_documentation.tl index edcef1e..3437c74 100644 --- a/scripts/gen_documentation.tl +++ b/scripts/gen_documentation.tl @@ -72,12 +72,12 @@ local _h1 = tag_wrapper "h1" local h2 = tag_wrapper "h2" local h3 = tag_wrapper "h3" local _h4 = tag_wrapper "h4" -local pre = tag_wrapper "pre" +local _pre = tag_wrapper "pre" local p = tag_wrapper "p" local a = tag_wrapper "a" local br = "
" -local function doc_header(content: string | TagTree, name: string): TagTree - return h3(a({ "", content, "" }, { name = name })) +local function bullet_header(content: string | TagTree, name: string): TagTree + return h3(a({ "", content, "" }, { name = name })) end local emit: {string:Emitter} = setmetatable({}, { @@ -117,22 +117,94 @@ local function escape_html_chars(str: string): string, integer return str:gsub("[<>'\"&]", html_escape_map) end +local function begin_signature_and_description(name: string, keyword: string): {string} + local data = { + "
", + "
",
+      "", assert(keyword), " ", assert(name),
+   }
+
+   return data
+end
+
+local function end_signature_and_description(
+   name: string,
+   link_name: string,
+   data: {string},
+   content: string | TagTree
+): string
+   return html {
+      bullet_header(name, link_name),
+      data,
+      "
", + content, + "
", + } +end + emit["function_statement"] = function(prefix: {string}, n: ts.Node, out: {string}): string local sig = n:child_by_field_name("signature") local ret = sig:child_by_field_name("return_type") local typeargs = sig:child_by_field_name("typeargs") local name = n:child_by_field_name("name"):source() + local data = begin_signature_and_description(name, "function") + local first = true + + if typeargs then + table.insert(data, (escape_html_chars "<")) + for child in typeargs:named_children() do + if not first then + table.insert(data, ", ") + end + table.insert(data, "") + table.insert(data, child:source()) + table.insert(data, "") + first = false + end + table.insert(data, (escape_html_chars ">")) + end + + table.insert(data, "(") + + first = true + for child in sig:child_by_field_name("arguments"):named_children() do + local parameter_name = child:child_by_field_name("name") + local ty = assert(child:child_by_field_name("type")):source() + if not first then + table.insert(data, ",") + end + table.insert(data, "\n\t") + table.insert(data, parameter_name and parameter_name:source() or "...") + table.insert(data, ": ") + table.insert(data, ty) + table.insert(data, "") + + first = false + end + + table.insert(data, first and ")" or "\n)") + + if ret then + table.insert(data, ": ") + first = true + for t in ret:named_children() do + if not first then + table.insert(data, ", ") + end + table.insert(data, "" .. t:source() .. "") + first = false + end + end + table.insert( out, - html { - doc_header({ name, - typeargs and escape_html_chars(typeargs:source()) or "", - sig:child_by_field_name("arguments"):source(), - (ret and ": " .. ret:source() or "") }, - name), + end_signature_and_description( + (name:match(":") and "method " or "function ") .. name, + name, + data, p { escape(table.concat(prefix)) } - } + ) ) return name end @@ -142,34 +214,30 @@ emit["enum_declaration"] = function(prefix: {string}, n: ts.Node, out: {string}) assertf(body, "enum_body is nil for %s", tostring(n)); local name = n:child_by_field_name("name"):source() - local entries = {} + + local data = begin_signature_and_description(name, "enum") + table.insert(data, "\n") for child in body:named_children() do - table.insert(entries, child:source()) + table.insert(data, "\t" .. child:source() .. "\n") end + table.insert(data, "end") table.insert( out, - html { - doc_header ({"type ", name}, name), - pre { "enum ", name, br, " ", - table.concat(entries, br .. " "), - br, - "end " }, - p { escape(table.concat(prefix)) } - } + end_signature_and_description("type " .. name, name, data, p { escape(table.concat(prefix)) }) ) return name end emit["record_declaration"] = function(prefix: {string}, n: ts.Node, out: {string}): string - local fields = {} local meta = {} local body = n:child_by_field_name("record_body") assertf(body, "record_body is nil for %s", tostring(n)) + local fields = {} local private_fields = {} for c in body:named_children() do @@ -187,55 +255,55 @@ emit["record_declaration"] = function(prefix: {string}, n: ts.Node, out: {string table.insert( is_private and private_fields or fields, (is_string and "[%s]" or "%s"):format(key:source()) - .. ": " .. t:source() + .. ": " .. t:source() .. "" ) elseif c:type() == "metamethod" then table.insert( meta, - c:source() + "metamethod " + .. c:child_by_field_name("name"):source() + .. ": " + .. c:child_by_field_name("type"):source() + .. "" ) elseif c:type() == "record_array_type" then table.insert( fields, 1, - "{" .. c:child(0):source() .. "}" + "{" .. c:child(0):source() .. "}" ) end end local obj_name = n:child_by_field_name("name"):source() - local pre_contents = { "record ", obj_name } + local data = begin_signature_and_description(obj_name, "record") if #fields > 0 then - table.insert(pre_contents, br .. " " .. table.concat(fields, br .. " ")) + table.insert(data, "\n\t" .. table.concat(fields, "\n\t")) end if #meta > 0 then if #fields > 0 then - table.insert(pre_contents, br) + table.insert(data, "\n") end - table.insert(pre_contents, br .. " " .. table.concat(meta, br .. " ")) + table.insert(data, "\n\t" .. table.concat(meta, "\n\t")) end if #private_fields > 0 then if #fields > 0 or #meta > 0 then - table.insert(pre_contents, br) + table.insert(data, br) end - table.insert(pre_contents, br .. "
-- private fields" .. br .. " ") - table.insert(pre_contents, table.concat(private_fields, br .. " ")) - table.insert(pre_contents, "
") + table.insert(data, "\n
\t-- private fields (click to show)\n\t") + table.insert(data, table.concat(private_fields, "\n\t")) + table.insert(data, "
") end - table.insert(pre_contents, br .. "end") + table.insert(data, "\nend") table.insert( out, - html { - doc_header ({"type ", obj_name}, obj_name), - pre (pre_contents), - p { escape(table.concat(prefix)) } - } + end_signature_and_description("type " .. obj_name, obj_name, data, p { escape(table.concat(prefix)) }) ) return obj_name end