EN | NL | 中文

Libs

object

%cookies

/phlo/libs/cookies.phlo
version 1.0
creator q-ai.nl
description Cookies data object
method

%cookies -> controller

line 7
This code assigns the contents of all cookies to an object property, allowing it to be reused later in the code.
this->objData = $_COOKIE
prop

%cookies -> lifetimeDays

line 5
This is a code that sets the value of the cookie's lifespan to 180 days.
180
method

%cookies -> __set ($key, $value)

line 9
This code ensures that a value is stored in the internal data, the global cookies, and via an HTTP-only, secure cookie with a certain lifespan.
$this->objData[$key] = $value
$_COOKIE[$key] = $value
setcookie($key, $value, time() + $this->lifetimeDays * 86400, slash, $_SERVER['HTTP_HOST'], true, true)
method

%cookies -> __unset ($key)

line 15
Removes a cookie and the associated data from the objects by unsetting the value and deleting the cookie with an expired timestamp.
unset($this->objData[$key], $_COOKIE[$key])
setcookie($key, void, time() - 86400, slash, $_SERVER['HTTP_HOST'], true, true)
object

%creds

/phlo/libs/creds.phlo
version 1.0
creator q-ai.nl
description INI file credentials handler
method

%creds -> __construct (?array $values = null)

line 5
In this constructor, if no values are provided, data is loaded from an INI file. Then, for each key-value pair in that array, a dynamic property is created: if the value is an array, a new instance of the class itself is created; otherwise, a SensitiveParameterValue object is created.
$values ??= parse_ini_file(data.'creds.ini', true)
foreach ($values AS $key => $value){
	$this->$key = is_array($value) ? new static($value) : new SensitiveParameterValue($value)
}
method

%creds -> objGet ($key)

line 12
This code checks whether the parameter `$key` is equal to `'toArray'`. In that case, a recursive loop is performed over `objData`, converting values of type `'SensitiveParameterValue'` using their `getValue()` method. If `$key` is not `'toArray'`, it checks whether an element exists under `$key` in `objData` that is of type `'SensitiveParameterValue'`, and then returns the value via `getValue()`.
if ($key === 'toArray') return loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? $value->getValue() : $value)
if (isset($this->objData[$key]) && is_a($this->objData[$key], 'SensitiveParameterValue')) return $this->objData[$key]->getValue()
method

%creds -> objInfo

line 17
Create a list through the input where each value is checked for the type 'SensitiveParameterValue'. If so, the value is replaced with a series of asterisks of the same length; otherwise, the value remains unchanged.
loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)
object

%encryption

/phlo/libs/encryption.phlo
version 1.0
creator q-ai.nl
description Simple encryption implementation
function

function encrypt ($data, $key):string

line 5
This code generates a random nonce, encrypts the data using a secret key function, and then encodes the result in Base64 for secure transmission or storage.
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

function decrypt ($encrypted, $key):string

line 6
Decode the base64-encoded input and check if the decoding is successful and the length is sufficient. If so, the nonce is extracted from the first bytes and the rest is used as ciphertext. Then, the secret data is decrypted using a hash of the provided key and the sodium_crypto_secretbox_open function. If any of the checks fail, false is returned.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
object

%field

/phlo/libs/field.phlo
type abstract class
version 1.0
creator q-ai.nl
description ORM field
function

function field ($type, ...$args)

line 6
This defines a function that creates a new field, where the function name is dynamically constructed based on the type. It uses the `$type` argument and additional arguments to generate a field of that specific type.
phlo("field_$type", ...$args, type: $type)
static

field :: __handle

line 8
This node always returns a null value, regardless of the input or context.
null
prop

%field -> title

line 10
It makes the first letter of the value of the variable $name uppercase.
ucfirst($this->name)
method

%field -> input ($record, $CMS)

line 12
Creates an input field with the type, name, and other attributes based on the properties of the object and fills in the value from the record or a default value.
input(type: $this->type, name: $this->name, value: $record->{$this->name} ?? $this->default, maxlength: $this->length, placeholder: $this->placeholder, class: 'field')
method

%field -> label ($record, $CMS)

line 13
This code returns the value of a property from the record, based on the name of the field.
$record->{$this->name};
object

%form_tags

/phlo/libs/form.tags.phlo
version 1.0
creator q-ai.nl
description DOM form tags for button, input, select, textarea
function

function button (...$args):string

line 5
Creates a 'button' element with the given arguments as attributes or content.
tag('button', ...$args)
function

function input (...$args):string

line 6
This node generates an HTML `<input>` element with the specified arguments as attributes or content.
tag('input', ...$args)
function

function select (...$args):string

line 7
Create a 'select' element with the given arguments.
tag('select', ...$args)
function

function textarea (...$args):string

line 8
Creates an HTML `<textarea>` element with the given arguments.
tag('textarea', ...$args)
object

%lang

/phlo/libs/lang.phlo
version 1.0
creator q-ai.nl
description Language/translation library
requires @cookies @JSON @OpenAI @INI
advice Use %lang in views to show current app lang (for example in links)
function

function nl ($text, ...$args)

line 8
Translate the provided text into Dutch, possibly with additional arguments for interpolation or context.
%lang->translation('nl', $text, ...$args)
function

function en ($text, ...$args)

line 9
This node translates the input text into English, using the same translation function with the provided text and arguments.
%lang->translation('en', $text, ...$args)
view

%lang ->

line 11
This piece of code performs a translation by using the application's language setting, ensuring the correct language is loaded for the user.
%app->lang
prop

%lang -> browser

line 13
This code selects the most suitable language code from the user's 'HTTP_ACCEPT_LANGUAGE' header by comparing the first language in the header with available languages in the application. If no suitable language is found, the default language from the first matching language is selected or null if there are no matches.
last($langs = array_filter(explode(comma, $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? void), fn($lang) => isset(%app->langs[substr($lang, 0, 2)])), $langs ? substr(current($langs), 0, 2) : null)
method

%lang -> cookie

line 14
Checks whether the language from the cookies exists in the available languages. If so, the language value is returned; otherwise, null is returned.
($lang = %cookies->lang) && %app->langs[$lang] ? $lang : null
prop

%lang -> model

line 16
A brief explanation is that this node probably contains a setting or parameter for using a smaller or optimized GPT-4 model, suitable for less demanding tasks or faster processing.
'gpt-4o-mini'
method

%lang -> translations

line 17
Initializes the translations for the current language, using the settings or data from '%app->lang' and 'langs'.
%INI(%app->lang, langs)
method

%lang -> detect ($text, $fallback = 'en')

line 19
The code performs a chat request to an AI model to identify the language of a text and return that identification as an ISO 639-1 code. If the response is not a valid 2-character code, a fallback language is used.
$res = %OpenAI->chat (
	model: $this->model,
	system: 'Analyseer welke taal deze tekst is en geef alleen de ISO 639-1 code van de taal terug, zonder andere data!',
	user: $text.lf.lf.'De ISO 639-1 code van de taal is: ',
	temperature: 0,
)->answer
return strlen($res) === 2 ? strtolower($res) : $fallback
method

%lang -> hash ($from, $text)

line 29
Creates a short, unique hash by first summarizing the text with a regex and uppercase letters, using the first part as a prefix, and then adding an MD5 hash of the entire text, so that the resulting code is both recognizable and unique.
$from.($short = substr(implode(regex_all('/[A-z0-9]+/', ucwords($text))[0]), 0, 8)).substr(md5($text), 0, 10 - strlen($short))
method

%lang -> translation ($from, $text, ...$args)

line 31
This method translates text from a specified language to the active language. If the source and target languages are the same, the text is returned directly. Otherwise, the text is split into lines, a hash is created for each line, and a translation is searched for. If none is found, an asynchronous translation is requested. The translations are combined and returned, with optional formatting if arguments are provided.
if ($from === %app->lang) $translation = $text
else {
	$translation = []
	foreach (explode(lf, $text) AS $line){
		if (trim($line)){
			$hash = $this->hash($from, $line)
			if (!$item = $this->translations->$hash) phlo_async('lang', 'asyncTranslation', $from, %app->lang, $item = $line)
		}
		else $item = void
		$translation[] = $item
	}
	$translation = implode(lf, $translation)
}
$translation = strtr($translation, ['\n' => lf])
return $args ? sprintf($translation, ...$args) : $translation
static

lang :: asyncTranslation ($from, $to, $text)

line 49
This code changes the application's language, generates a unique hash based on the original language and text, translates the text, and stores the translation in a translation structure with the hash as the key.
%app->lang = $to
$hash = $this->hash($from, $text)
$translation = $this->translate($from, $to, $text)
return $this->translations->$hash = $translation
method

%lang -> translate ($from, $to, $text)

line 56
This function first checks if the source and target languages are the same and returns the original text in that case. Otherwise, it makes a chat call to OpenAI, instructing the model to translate the text between the specified languages, preserving markdown, punctuation, and capitalization, and only returns the translated text.
if ($from === $to) return $text
return %OpenAI->chat (
	model: $this->model,
	system: "You will be provided with a word, sentence or (markdown) text in ISO 639-1 language $from, and your task is to translate this string into ISO 639-1 language $to. Respect markdown, missing interpunction and specific use of capitals. Give only the translation.",
	user: $text,
	temperature: 0,
)->answer
object

%model

/phlo/libs/model.phlo
version 1.1
creator q-ai.nl
description Phlo ORM class (unified columns/schema)
type abstract class
requires @DB @MySQL apcu?
static

model :: DB

line 8
Connects to a MySQL database.
%MySQL
static

model :: objRecords

line 9
An empty array means that there are currently no records or items present in this node.
[]
static

model :: objLoaded

line 10
It seems to return an empty array, possibly intended to indicate that no data has been loaded or that no action has been performed.
[]
static

model :: objCache

line 11
This code always returns the value 'false'.
false
static

model :: columns

line 13
Checks if the static property `$columns` exists; if so, it is returned. If not, it checks whether a `schema` method exists; if so, this method is called via `_columns()`. If neither is the case, the string '*' is returned.
isset(static::$columns) ? static::$columns : (method_exists(static::class, 'schema') ? static::_columns() : '*')
static

model :: _columns

line 14
This code generates a quoted list of column names for database use, excluding only fields with specific types ('child', 'many', 'virtual'). It combines field definitions and adds the correct quotes for use in an SQL query.
$fq = static::DB()->fieldQuotes
$list = array_merge(...array_values(array_filter(loop(static::fields(), fn($field, $column) => in_array($field->type, ['child', 'many', 'virtual']) ? null : ($field->columns ?: [static::$table."$fq.$fq".$column])))))
return $fq.implode("$fq,$fq", $list).$fq
static

model :: fields

line 19
Checks if a 'schema' method exists in the class. If so, the output of '_fields()' is used; otherwise, it looks for the static property '$fields' or an empty array if it does not exist.
method_exists(static::class, 'schema') ? static::_fields() : (static::$fields ?? [])
static

model :: _fields

line 20
Loops through the schema fields and selects for each field the latest value from a chain of alternatives: either the name, or the object (for conditions with type 'parent'), or the field itself.
loop(static::schema(), fn($field, $column) => last($field->name ??= $column, $field->type === 'parent' && $field->obj ??= $column, $field))
static

model :: field ($name)

line 21
Returns the field configuration from the array of all fields, based on the specified name.
static::fields()[$name]
static

model :: create (...$args)

line 23
This code creates a new record and then returns it, possibly for further processing or as the result of the function.
static::record(id: static::createRecord(...$args))
static

model :: createRecord (...$args)

line 24
Creates a new database record in the specified table with the provided arguments.
static::DB()->create(static::$table, ...$args)
static

model :: change ($where, ...$args)

line 25
This code calls a static method 'change' that likely performs an update in the database for a specific table. It uses the database connection and table name from static properties and accepts conditions and additional arguments to execute the change.
static::DB()->change(static::$table, $where, ...$args)
static

model :: delete ($where, ...$args)

line 26
Performs a delete operation on the database for the specified table, based on the given conditions and any additional arguments.
static::DB()->delete(static::$table, $where, ...$args)
method

%model -> objSave

line 28
Check if an id exists; if not, display an error message. If a record with that id exists, it is updated and returned. Otherwise, a new record is created and returned.
$this->id || error('Can\'t save '.static::class.' record without an id')
if (static::item(id: $this->id, columns: 'id')){
	static::change('id=?', $this->id, ...$this)
	return static::record(id: $this->id)
}
else return static::create(...$this)
static

model :: column (...$args)

line 37
Loads multiple records and returns only the column values, without Object-Or-Array structure.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_COLUMN])
static

model :: item (...$args)

line 38
Loads records with the specified arguments, uses the 'fetch' mode, and retrieves only one column.
static::recordsLoad($args, 'fetch', [PDO::FETCH_COLUMN])
static

model :: pair (...$args)

line 39
Loads multiple records and returns them as a key-value pair, with the fetch mode set to FETCH_KEY_PAIR for quick and easy maps.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_KEY_PAIR])
static

model :: records (...$args)

line 40
Loads multiple records from the database and returns them as unique objects of the class, with all records merged into a single collection.
static::recordsLoad($args, 'fetchAll', [PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, static::class], true)
static

model :: recordCount (...$args)

line 41
This code counts the number of records by performing a count on the 'id' column.
static::item(...$args, columns: 'COUNT(id)')
static

model :: record (...$args)

line 42
Calculates the number of records and returns an error message if there is more than one record; otherwise, it returns the first record or null if there are no records.
count($records = static::records(...$args)) > 1 ? error('Multiple records for '.static::class) : (current($records) ?: null)
static

model :: recordsLoad ($args, $fetch, $fetchMode, $saveRelations = false)

line 44
This code dynamically constructs query parameters for loading records from a database, including conditions, joins, selections, and grouping rules, depending on static properties and arguments. It then uses caching with APCu based on a cache key and duration, making repeated loading more efficient. Finally, the loaded records are stored for reuse and returned.
$args['table'] ??= static::$table
$saveRelations && $args['columns'] ??= static::$table.'.id as _,'.static::columns()
isset(static::$joins) && $args['joins'] = static::$joins.(isset($args['joins']) ? " $args[joins]" : void)
method_exists(static::class, 'where') && $args['where'] = static::where().(isset($args['where']) ? " AND $args[where]" : void)
isset(static::$group) && $args['group'] ??= static::$group
isset(static::$order) && $args['order'] ??= static::$order
if ($cacheKey = $args['cacheKey'] ?? null) unset($args['cacheKey'])
if ($duration = static::$objCache) $records = apcu($cacheKey ?? static::class.slash.md5(json_encode($args)), fn() => static::DB()->load(...$args)->$fetch(...$fetchMode), $duration === true ? 86400 : $duration)
else $records = static::DB()->load(...$args)->$fetch(...$fetchMode)
if ($saveRelations && $records) self::$objRecords[static::class] = (self::$objRecords[static::class] ?? []) + array_column($records, null, 'id')
return $records
static

model :: objRel ($key)

line 58
Provides the value of a static property or method or, if not present, a default value (empty array).
static::$classProps[static::class][$key] ??= method_exists(static::class, $key) ? static::$key() : static::$$key ?? []
prop

%model -> objState

line 60
Initializes a structure with three empty collections: 'parents', 'children', and 'many'.
['parents' => [], 'children' => [], 'many' => []]
method

%model -> objGet ($key)

line 61
First search in the parent, then in the children, and if no result is found, then search multiple items.
$this->getParent($key) ?? $this->getChildren($key) ?? $this->getMany($key)
method

%model -> objIn ($ids)

line 62
Converts an array of IDs into a comma-separated string enclosed in quotes, or returns 'NULL' if the array does not exist or is empty.
$ids ? dq.implode(dq.comma.dq, $ids).dq : 'NULL'
method

%model -> getParent ($key)

line 64
Returns the direct parent object based on a key, with checks on previously loaded relationships and dynamically fetching related records if they are not yet loaded.
if (array_key_exists($key, $this->objState['parents'])) return $this->objState['parents'][$key]
$parents = self::objRel('objParents')
if (!$relation = $parents[$key] ?? null) return
$isArray = is_array($relation)
$class = $isArray ? $relation['obj'] : $relation
$column = $isArray ? $relation['key'] ?? $key : $key
if (!$parentId = $this->objData[$column] ?? null) return $this->objState['parents'][$key] = null
if (!isset(self::$objRecords[$class][$parentId])){
	$idsToLoad = [$parentId => true]
	$allObjData = array_map(fn($record) => $record->objData, self::$objRecords[static::class] ?? [])
	foreach ($parents as $pKey => $pRelation){
		$pIsArray = is_array($pRelation)
		$pClass = $pIsArray ? $pRelation['obj'] : $pRelation
		if ($pClass === $class) foreach (array_column($allObjData, $pIsArray ? $pRelation['key'] ?? $pKey : $pKey) as $pId) $pId && !isset(self::$objRecords[$class][$pId]) && $idsToLoad[$pId] = true
	}
	if ($idsToLoad = array_keys($idsToLoad)) $class::records(where: 'id IN ('.$this->objIn($idsToLoad).')')
}
$parentObject = self::$objRecords[$class][$parentId] ?? null
return $this->objState['parents'][$key] = $parentObject
method

%model -> getChildren ($key)

line 86
Verifies whether the children for a given key are already loaded; if not, the relationship is requested and the corresponding records are loaded and stored in the state; finally, the children array is returned.
if (array_key_exists($key, $this->objState['children'])) return $this->objState['children'][$key]
if (!$relation = self::objRel('objChildren')[$key] ?? null) return
$isArray = is_array($relation)
$class = $isArray ? $relation['obj'] : $relation
$column = $isArray ? $relation['key'] : static::class
if (!isset(self::$objLoaded[static::class]['children'][$key])){
	$parentIds = array_keys(self::$objRecords[static::class] ?? [])
	if ($parentIds){
		$children = $class::records(where: '`'.$column.'` IN ('.$this->objIn($parentIds).')')
		foreach (self::$objRecords[static::class] AS $parentRecord) $parentRecord->objState['children'][$key] = []
		foreach ($children AS $childId => $child) !is_null($pId = $child->objData[$column] ?? null) && isset(self::$objRecords[static::class][$pId]) && self::$objRecords[static::class][$pId]->objState['children'][$key][$childId] = $child
	}
	self::$objLoaded[static::class]['children'][$key] = true
}
return $this->objState['children'][$key] ?? []
method

%model -> getMany ($key)

line 104
Load multiple related records from the database if they have not been loaded before and cache them in the object state, so that subsequent calls have the records available immediately.
if (array_key_exists($key, $this->objState['many'])) return $this->objState['many'][$key]
if (!$relation = self::objRel('objMany')[$key] ?? null) return
$class = $relation['obj']
if (!isset(self::$objLoaded[static::class]['many'][$key])){
	$parentIds = array_keys(self::$objRecords[static::class] ?? [])
	if ($parentIds){
		$targetTable = $class::$table
		$records = $class::recordsLoad(arr(table: $relation['table'], columns: "`$targetTable`.*, `$relation[table]`.`$relation[localKey]` as _local_key", joins: "INNER JOIN `$targetTable` ON `$relation[table]`.`$relation[foreignKey]` = `$targetTable`.`id`", where: "`$relation[table]`.`$relation[localKey]` IN (".$this->objIn($parentIds).")"), 'fetchAll', [PDO::FETCH_CLASS, $class])
		foreach (self::$objRecords[static::class] AS $parentRecord) $parentRecord->objState['many'][$key] = []
		foreach ($records AS $record){
			$recordId = $record->id
			$parentId = $record->_local_key
			unset($record->_local_key)
			if (isset(self::$objRecords[static::class][$parentId])) self::$objRecords[static::class][$parentId]->objState['many'][$key][$recordId] = $record
		}
	}
	self::$objLoaded[static::class]['many'][$key] = true
}
return $this->objState['many'][$key] ?? []
method

%model -> getCount ($key)

line 126
This code first checks if the count is already stored in the internal state and returns it if available. Then it identifies whether the count relates to a child relationship ('objChildren') or a multiple relationship ('objMany'). If it is a child relationship, it retrieves the IDs of linked records, performs an aggregated query to determine the count, and caches these results. For a 'many' relationship, similar logic is applied but through a separate table and column. If no relationship is found, 0 is returned.
if (array_key_exists($key, $this->objState['counts'] ?? [])) return $this->objState['counts'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['children_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$counts = $class::pair(columns: "`$column`, COUNT(*)", where: '`'.$column.'` IN ('.$this->objIn($parentIds).')', group: "`$column`")
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['children_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
if ($relation = self::objRel('objMany')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['many_count'][$key])){
		$parentIds = array_keys(self::$objRecords[static::class] ?? [])
		if ($parentIds){
			$counts = static::DB()->load(table: $relation['table'], columns: "`$relation[localKey]`,COUNT(*)", where: '`'.$relation['localKey'].'` IN ('.$this->objIn($parentIds).')', group: "`$relation[localKey]`")->fetchAll(PDO::FETCH_KEY_PAIR)
			foreach (self::$objRecords[static::class] as $id => $record) $record->objState['counts'][$key] = (int)($counts[$id] ?? 0)
		}
		self::$objLoaded[static::class]['many_count'][$key] = true
	}
	return $this->objState['counts'][$key] ?? 0
}
return 0
method

%model -> getLast ($key)

line 156
Searches in the object status whether the last child linked to the specified key has already been loaded; if not, the last child is retrieved via relationship data and records, stored in the status, and returned.
if (array_key_exists($key, $this->objState['last_child'] ?? [])) return $this->objState['last_child'][$key]
if ($relation = self::objRel('objChildren')[$key] ?? null){
	if (!isset(self::$objLoaded[static::class]['last_child'][$key])){
		if ($parentIds = array_keys(self::$objRecords[static::class] ?? [])){
			$isArray = is_array($relation)
			$class = $isArray ? $relation['obj'] : $relation
			$column = $isArray ? $relation['key'] : static::class
			$childTable = $class::$table
			$whereClause = "`$column` IN (".$this->objIn($parentIds).") AND `$childTable`.`id` = (SELECT `id` FROM `$childTable` AS lc WHERE lc.`$column`=`$childTable`.`$column` ORDER BY `id` DESC LIMIT 1)"
			$lastChildren = $class::records(where: $whereClause)
			foreach (self::$objRecords[static::class] as $record) $record->objState['last_child'][$key] = null
			foreach ($lastChildren as $child) if (isset(self::$objRecords[static::class][$parentId = $child->objData[$column]])) self::$objRecords[static::class][$parentId]->objState['last_child'][$key] = $child
		}
		self::$objLoaded[static::class]['last_child'][$key] = true
	}
	return $this->objState['last_child'][$key] ?? null
}
return null
static

model :: objParents

line 177
Retrieves the static property if it exists; otherwise, it returns an empty array if the 'schema' method does not exist; if it does, it filters the fields of type 'parent' and creates an array, returning an object for each field with an optional key or using the object name.
if (property_exists(static::class, 'objParents')) return static::$objParents
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'parent'), fn($f, $c) => $f->key ? arr(obj: $f->obj, key: $f->key) : ($f->obj ?? $c))
static

model :: objChildren

line 183
This code checks if the static property 'objChildren' exists and returns it if present. If the 'schema' method does not exist, it returns an empty array. Otherwise, an array is filtered for fields with type 'child', and these are then processed by a loop function that determines whether to return an object and possibly a key, or just the object itself.
if (property_exists(static::class, 'objChildren')) return static::$objChildren
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'child'), fn($f, $c) => $f->key ? arr(obj: $f->obj, key: $f->key) : ($f->obj ?? $c))
static

model :: objMany

line 189
Returns the value of the static property `objMany` if it exists; otherwise, if there is no `schema` method, it returns an empty array; otherwise, it filters the fields of type 'many' and constructs an array with object and table information, including the local and foreign keys.
if (property_exists(static::class, 'objMany')) return static::$objMany
if (!method_exists(static::class, 'schema')) return []
return loop(array_filter(static::fields(), fn($f) => $f->type === 'many'), fn($f) => arr(obj: $f->obj, table: $f->table, localKey: $f->localKey ?? static::class, foreignKey: $f->foreignKey ?? $f->obj))
static

model :: createTable

line 195
Checks if the method schema() exists, otherwise an error is thrown. Then a SQL CREATE TABLE statement is constructed by iterating over and formatting the field data, including any NOT NULL or NULL specifications, and adds a primary key on 'id'.
method_exists(static::class, 'schema') || error(static::class.' has no schema()')
return 'CREATE TABLE `'.static::$table.'` ('.lf.tab.implode(",\n\t", array_merge(...array_values(array_filter(loop(static::fields(), fn($field) => loop((array)$field->sql, fn($sql) => $sql.($field->required || $field->nullable === false ? ' NOT' : void).' NULL')))))).",\n\tPRIMARY KEY (`id`)\n)"
object

%n8n

/phlo/libs/n8n.phlo
version 1.0
creator q-ai.nl
description Simple n8n functions
function

function n8n ($webhook, ?array $data = null, $test = false)

line 5
Makes a POST request to a webhook URL, based on server data and test mode, with optional data included.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

function n8n_test ($webhook, ?array $data = null)

line 6
Call a function named 'n8n' with the webhook, an optional data array, and a boolean parameter (probably for a special mode or debug).
n8n($webhook, $data, true)
object

%payload

/phlo/libs/payload.phlo
version 1.0
creator q-ai.nl
description POST/PUT/PATCH and file-upload data object
requires @file
method

%payload -> controller

line 7
Parses JSON request bodies for POST, PUT, PATCH methods and performs import; handles multipart/form-data uploads with file data, temporarily stores files, and structures the data, including array collections; also processes standard $_FILES data through import.
f (in_array(method, ['POST', 'PUT', 'PATCH']) && str_starts_with($_SERVER['CONTENT_TYPE'] ?? void, 'application/json')) return $this->objData = get_object_vars(json_read('php://input'))
if ($_POST) $this->objImport(...$_POST)
elseif (method === 'PUT' && str_starts_with($_SERVER['CONTENT_TYPE'], 'multipart/form-data')){
$boundary = '--'.regex('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'])[1]
$arrays = []
foreach (explode($boundary, file_get_contents('php://input')) AS $part){
	if (!trim($part) || $part === '--' || !str_contains($part, nl.nl)) continue
	[$rawHeaders, $body] = explode(nl.nl, $part, 2)
	foreach (explode(nl, trim($rawHeaders)) AS $header){
		if (str_contains($header, colon)){
			[$key, $value] = explode(colon, $header, 2)
			$headers[strtolower(trim($key))] = trim($value)
		}
	}
	if (!isset($headers['content-disposition'])) continue
	if (!preg_match('/name="([^"]+)"/', $headers['content-disposition'], $match)) continue
	$name = $match[1]
	$body = rtrim($body, nl) ?: null
	if (str_ends_with($name, '[]')) $arrays[] = substr($name, 0, -2)
	if (preg_match('/filename="([^"]*)"/', $headers['content-disposition'], $f)){
		if ($f[1] === void || $body === null){
			if (!str_ends_with($name, '[]')) $this->objData[$name] = null
			continue
		}
		$filename = $f[1]
		$file = %file(tempnam(sys_get_temp_dir(), 'phlo'), $filename, $body)
		if (str_ends_with($name, '[]')) $this->objData[substr($name, 0, -2)][] = $file
		else $this->objData[$name] = $file
	}
	else {
		if (str_ends_with($name, '[]')) $this->objData[substr($name, 0, -2)][] = $body
		else $this->objData[$name] = $body
	}
}
foreach ($this->objData AS $key => $val){
	if (str_ends_with($key, '[]')){
		unset($this->objData[$key])
		$this->objData[substr($key, 0, -2)] = is_array($val) ? array_values(array_filter($val, fn($v) => $v !== null)) : [$val]
	}
	elseif (!is_array($val) && substr($key, -2) === '[]') $this->objData[$key] = [$val]
}
foreach (array_unique($arrays) AS $key) if (!isset($this->objData[$key])) $this->objData[$key] = []
}
if ($_FILES) $this->objImport(...loop($_FILES, fn($f) => is_array($f['name']) ? loop(array_keys($f['name']), fn($i) => $f['error'][$i] ? null : %file($f['tmp_name'][$i], $f['name'][$i], mime: $f['type'][$i], size: $f['size'][$i])) : ($f['error'] ? null : %file($f['tmp_name'], $f['name'], mime: $f['type'], size: $f['size']))))
object

%security

/phlo/libs/security.phlo
version 1.0
creator q-ai.nl
description Generic security library
prop

%security -> nonce

line 5
Generate a random 8-character token and store it in the `$nonce` property.
%app->nonce = token(8)
method

%security -> full

line 7
Provides a collection of security headers or settings by checking or assigning various properties, presumably focused on web security and privacy.
$this->COOP
$this->CORP
$this->CORS
$this->CSP
$this->Referrer
$this->X_content
$this->X_frame
method

%security -> CSRF

line 17
Generates and stores a new 12-character CSRF token in the session.
%session->csrf = token(12)
method

%security -> COOP

line 18
Sets an HTTP header that specifies that the page can only be opened by documents from the same origin, providing security improvements against certain attack techniques such as side-channel attacks.
header('Cross-Origin-Opener-Policy: same-origin')
method

%security -> CORP

line 19
Adds an HTTP header indicating that Cross-Origin Resource Sharing is only allowed from the same origin.
header('Cross-Origin-Resource-Policy: same-origin')
method

%security -> CORS ($host = host)

line 20
Sets the allowed origin for cross-origin resource sharing (CORS) by adding a header that only permits access from the specified host.
header("Access-Control-Allow-Origin: https://$host")
method

%security -> CSP

line 21
Sends a Content-Security-Policy header that imposes restrictions on sources for content, scripts, styles, images, fonts, connections, and frame-ancestors, including a nonce for dynamically loaded scripts.
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-$this->nonce' 'unsafe-inline'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'")
method

%security -> Referrer

line 22
It sets an HTTP header that specifies that the referrer information is only sent to the same origin or to third parties via a full URL, but not in cross-origin requests, thereby enhancing privacy.
header('Referrer-Policy: strict-origin-when-cross-origin')
method

%security -> X_content

line 23
Adds an HTTP header that prevents browsers from guessing the response's content-type, contributing to improved security by preventing content sniffing.
header('X-Content-Type-Options: nosniff')
method

%security -> X_frame

line 24
This code sets the HTTP header 'X-Frame-Options' to 'DENY', preventing the page from being loaded in a frame or iframe, which contributes to protection against clickjacking.
header('X-Frame-Options: DENY')
object

%session

/phlo/libs/session.phlo
version 1.0
creator q-ai.nl
description Session data object
method

%session -> controller

line 5
Start the session and store the contents of the $_SESSION variable in a property for later use.
ession_start()
$this->objData = $_SESSION
method

%session -> __set ($key, $value)

line 8
This code modifies two arrays simultaneously: a global session array and an internal data array of the object, by assigning the given value to the specified key in both arrays.
$_SESSION[$key] = $this->objData[$key] = $value
method

%session -> __unset ($key)

line 9
Removes a key from both the internal data structure and the session data.
unset($this->objData[$key], $_SESSION[$key])

AI

object

%Claude

/phlo/libs/AI/Claude.phlo
version 1.0
creator q-ai.nl
description Antrophic Claude API (beta)
requires creds:Claude
static

Claude :: context (...$args)

line 7
Add 'assistant' and 'user' content to the 'messages' array if present, then remove these keys from the array.
$args['messages'] ??= []
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
method

%Claude -> chat (...$args)

line 14
This code makes a configured request to a chat model, setting default values for the model and the maximum number of tokens. Then, a request is made and the response is returned as an object with an `answer` field.
$args = static::context(...$args)
$args['model'] ??= 'claude-3-5-sonnet-latest'
$args['max_tokens'] ??= 3333
$res = $this->request('messages', true, POST: $args)
return obj(answer: $res->content[0]->text)
method

%Claude -> stream (...$args)

line 22
This function establishes a streaming connection with an API endpoint, sends a POST request with specified parameters, and processes the received data in real-time via a callback. If no callback is provided, an event stream is automatically set up and outgoing data is displayed directly. The response is constructed from the received delta texts and returned as complete text.
%app->streaming = true
$args = static::context(...$args)
$args['model'] ??= 'claude-3-5-sonnet-latest'
$args['max_tokens'] ??= 3333
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->delta->text ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$curl = curl_init('https://api.anthropic.com/v1/messages')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['anthropic-version: 2023-06-01', 'Content-Type: application/json', 'Content-Length: '.strlen($data), 'X-Api-Key: '.%creds->Claude])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$answer){
	foreach (array_filter(explode(lf.lf, $data)) AS $chunk){
		$obj = json_decode(substr($chunk, strpos($chunk, 'data: {') + 6))
		$res = $cb($obj, $obj->delta->text ?? null)
		if (is_string($res)) $answer .= $res
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%Claude -> request ($uri, $JSON = true, ...$args)

line 53
Sends an HTTP request to a specified API endpoint with default headers and API key, decodes the JSON result, checks for errors, and returns the decoded object.
$res = json_decode(HTTP("https://api.anthropic.com/v1/$uri", ['anthropic-version: 2023-06-01', 'Content-Type: application/json', 'X-Api-Key: '.%creds->Claude], $JSON, ...$args))
if (isset($res->error)) error('Claude Request error: '.$res->error->message)
return $res
object

%DeepSeek

/phlo/libs/AI/DeepSeek.phlo
version 1.0
creator q-ai.nl
description Basic DeepSeek functions
requires creds:DeepSeek
static

DeepSeek :: context (...$args)

line 7
Adds default values to the 'messages' array and adds conversation roles such as 'system', 'assistant', and 'user' based on the provided arguments, then cleans up the original arguments.
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
method

%DeepSeek -> chat (...$args)

line 15
Processes a chat request by preparing arguments, sends a request to the chat completions endpoint, and returns an object with model information, finish reason, token usage, and content or tool calls.
$args = static::context(...$args)
$res = $this->request('chat/completions', POST: $args)
$return = obj(model: $res->model, finish: $res->choices[0]->finish_reason, tokens: $res->usage->total_tokens, tokens_in: $res->usage->prompt_tokens, tokens_out: $res->usage->completion_tokens)
if (isset($res->choices[0]->message->tool_calls)) $return->tools = loop($res->choices[0]->message->tool_calls, fn($tool) => obj(name: $tool->function->name, args: json_decode($tool->function->arguments, true)))
else $return->answer = $res->choices[0]->message->content
return $return
method

%DeepSeek -> stream (...$args)

line 24
This method establishes a streaming connection with an API, processes temporary data uploads via cURL, and uses a callback to process and merge real-time data into a final response.
%app->streaming = true
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($data) => last(($text = $data->choices[0]->delta->content ?? void) === void || [print($text), cli || [ob_flush(), flush()]], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.deepseek.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $data = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->DeepSeek, 'Content-Type: application/json', 'Content-Length: '.strlen($data)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if (!str_starts_with($chunk, 'data: ')){
			$buffer = $chunk
			continue
		}
		if ($obj = json_decode(substr($chunk, 6))){
			$res = $cb($obj)
			if (is_string($res)) $answer .= $res
		}
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%DeepSeek -> request ($uri, $JSON = true, ...$args)

line 62
Sends an HTTP request to a DeepSeek API endpoint with a specified URI and authentication. Then decodes the JSON response and checks for errors; if present, an error is generated. Afterwards, the response is returned.
$res = json_decode(HTTP("https://api.deepseek.com/v1/$uri", ['Authorization: Bearer '.%creds->DeepSeek], $JSON, ...$args))
if (isset($res->error)) error('DeepSeek Request error: '.$res->error->message)
return $res
object

%Gemini

/phlo/libs/AI/Gemini.phlo
version 1.0
creator q-ai.nl
description Basic Gemini image functions (experimental)
requires creds:Gemini
method

%Gemini -> config ($modalities)

line 7
Assembles a configuration with parameters for language output, such as temperature, topK, topP, maximum tokens, and response modes and mime-types.
arr(temperature: 1, topK: 40, topP: .95, maxOutputTokens: 8192, response_modalities: $modalities, response_mime_type: 'text/plain')
method

%Gemini -> change ($prompt, $base64, $type = 'image/jpeg')

line 9
Sends a POST request to the Google API for generative images with the specified prompt and base64 image, receives the JSON response, and returns it as the result.
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(inlineData: arr(data: $base64, mimeType: $type))]),
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['image', 'text']),
			),
		),
	),
)
method

%Gemini -> create ($prompt)

line 25
Sends a POST request to the Google generative language API for content generation based on a prompt, and processes the JSON response to return it.
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['image', 'text']),
			),
		),
	),
)
method

%Gemini -> info ($prompt, $base64, $type = 'image/jpeg')

line 40
Sends a POST request to the Google API for generative image content, with base64 image and prompt as input, and decodes the JSON response before returning it.
$this->respond (
	json_decode (
		HTTP (
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key='.%creds->Gemini,
			JSON: true,
			POST: arr (
				contents: [
					arr(role: 'user', parts: [arr(inlineData: arr(data: $base64, mimeType: $type))]),
					arr(role: 'user', parts: [arr(text: $prompt)]),
				],
				generationConfig: $this->config(['text']),
			),
		),
	),
)
method

%Gemini -> respond ($res)

line 56
Checks if the answer ends with an 'IMAGE_SAFETY' rating and issues a warning if so. If not, returns the first found textual content or inline data. If there is no valid information, returns an error message.
if ($res->candidates[0]->finishReason === 'IMAGE_SAFETY') return 'I\'m affraid I can\'t process your image or command, it seems unsafe.'
if ($text = $res->candidates[0]->content->parts[0]->text ?? null) return $text
if ($data = $res->candidates[0]->content->parts[0]->inlineData ?? null) return $data
return 'Some error occured processing your request, please try again later.'
object

%OpenAI

/phlo/libs/AI/OpenAI.phlo
version 1.0
creator q-ai.nl
description Basic OpenAI functions
requires creds:OpenAI
const

OpenAI :: model

line 7
This is a specification for a model setting that indicates it uses the GPT-4 mini model.
'gpt-4o-mini'
const

OpenAI :: voices

line 8
This is a list of string elements containing different voice or voice names, possibly for use in a text-to-speech function or voice selection module.
['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']
static

OpenAI :: context (...$args):array

line 10
Adds messages to the message list based on the input parameters (system, assistant, user), ensures that the messages are correctly ordered, and removes the original parameters after adding.
$args['messages'] ??= []
if (isset($args['system']) && array_unshift($args['messages'], arr(role: 'system', content: $args['system']))) unset($args['system'])
if (isset($args['assistant']) && array_push($args['messages'], arr(role: 'assistant', content: $args['assistant']))) unset($args['assistant'])
if (isset($args['user']) && array_push($args['messages'], arr(role: 'user', content: $args['user']))) unset($args['user'])
return $args
static

OpenAI :: tool ($tool):array

line 18
Create a descriptive object structure with name, description, and parameters, where parameters are generated from a list of arguments and contain only specific properties, excluding all additional properties.
arr (
	type: 'function',
	function: arr (
		name: $tool->name,
		description: $tool->desc,
		parameters: arr (
			type: 'object',
			properties: loop($tool->args, fn($data, $arg) => array_filter($data, fn($key) => in_array($key, ['type', 'enum', 'desc']), ARRAY_FILTER_USE_KEY)),
			additionalProperties: false,
			required: array_keys($tool->args),
		),
		strict: true,
	),
)
method

%OpenAI -> chat (...$args):obj

line 33
Processes a chat request by determining the model and context, sends a request to the API, and returns an object with details about the model, the reason for completion, tokens, and the content or tool calls, depending on the response.
$args['model'] ??= static::model
$args = static::context(...$args)
$res = $this->request('chat/completions', POST: $args)
$return = obj(model: $res->model, finish: $res->choices[0]->finish_reason, tokens: $res->usage->total_tokens, tokens_in: $res->usage->prompt_tokens, tokens_out: $res->usage->completion_tokens)
if (isset($res->choices[0]->message->tool_calls)) $return->tools = loop($res->choices[0]->message->tool_calls, fn($tool) => obj(name: $tool->function->name, args: json_decode($tool->function->arguments, true)))
else $return->answer = $res->choices[0]->message->content
return $return
method

%OpenAI -> embedding ($input, $model = 'text-embedding-3-small')

line 43
Performs a POST request to the 'embeddings' endpoint with the input and the model, and returns the first embedding from the response.
$this->request('embeddings', POST: arr(input: $input, model: $model))->data[0]->embedding
method

%OpenAI -> stream (...$args):string

line 45
Establishes a streaming connection with the OpenAI API, sends a chat request, and processes the received data by passing it in real-time via a callback, building and returning textual fragments.
%app->streaming = true
$args['model'] ??= static::model
$args = static::context(...$args)
$args['stream'] = true
if (isset($args['cb'])){
	$cb = $args['cb']
	unset($args['cb'])
}
else {
	cli || header('Content-Type: text/event-stream')
	$cb = fn($text) => last(print($text), cli || [@ob_flush(), flush()], $text)
}
$answer = void
$buffer = void
$curl = curl_init('https://api.openai.com/v1/chat/completions')
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST')
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload = json_encode($args))
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.%creds->OpenAI, 'Content-Type: application/json', 'Content-Length: '.strlen($payload)])
curl_setopt($curl, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($cb, &$buffer, &$answer){
	$chunks = trim($buffer.$data)
	$buffer = void
	foreach (explode(lf.lf, $chunks) AS $chunk){
		if ($obj = json_decode(substr($chunk, 6))) in_array($text = $obj->choices[0]->delta->content ?? null, [null, void]) || $answer .= $cb($text, $obj)
		else $buffer = $chunk
	}
	return strlen($data)
})
curl_exec($curl)
return $answer
method

%OpenAI -> transcribe ($file, $model = 'whisper-1', ...$args):obj

line 77
Processes an audio file by converting it to CURLFile if necessary, sends it to the API for transcription with the selected model, and returns an object with model name, duration, language, and transcription text.
if (is_string($file)) $file = new CURLFile($file)
elseif (is_a($file, 'file')) $file = $file->curl
$res = $this->request('audio/transcriptions', false, POST: arr(...$args, model: $model, file: $file, response_format: 'verbose_json'))
return obj (
	model: $model,
	duration: $res->duration,
	lang: $res->language,
	text: $res->text,
)
method

%OpenAI -> vision ($text, $image, $stream = false, ...$args):obj

line 89
Compose a message with text and optionally an image, and choose based on the stream parameter for a continuous stream or a chat response.
$args['model'] ??= static::model
$messages = [arr(role: 'user', content: [arr(type: 'text', text: $text), arr(type: 'image_url', image_url: arr(url: $image))])]
if ($stream) return $this->stream(...$args, messages: $messages)
else return $this->chat(...$args, messages: $messages)
method

%OpenAI -> request ($uri, $JSON = true, ...$args)

line 96
Sends an HTTP request to the OpenAI API with the given URI and options. Decodes the JSON response, checks for errors, and returns the result.
$res = json_decode(HTTP("https://api.openai.com/v1/$uri", ['Authorization: Bearer '.%creds->OpenAI], $JSON, ...$args))
if (isset($res->error)) error('OpenAI Request error:'.lf.$res->error->message)
return $res

CSS

object

%basics

/phlo/libs/CSS/basics.phlo
version 1.0
creator q-ai.nl
description Single Page App basic CSS boilerplate
view

style

line 5
Statically assigned CSS class selector that contains various style rules for text alignment, floats, positioning, margins, paddings, overflow, display, cursor, text decoration, and width, intended for reuse and consistent styling.
.left: text-align: left
.center: text-align: center
.right: text-align: right

.float-left: float: left
.float-right: float: right

.absolute: position: absolute
.fixed: position: fixed
.relative: position: relative
.sticky: position: sticky

.padded: padding: 1rem
.margin: margin: 1rem
.margin-auto: margin: auto

.overflow: overflow: auto
.overflow-x: overflow-x: auto
.overflow-y: overflow-y: auto
.hide-x: overflow-x: hidden
.hide-y: overflow-y: hidden

.block: display: block
.hidden: display: none

.pointer: cursor: pointer
.underline: text-decoration: underline

.fit: width: fit-content
.full: width: 100%
.wide: max-width: 1200px
object

%fixes

/phlo/libs/CSS/fixes.phlo
version 1.0
creator q-ai.nl
description Single Page App basic CSS boilerplate fixes
view

style

line 5
Apply box-sizing to all elements and pseudo-elements so that they include padding and border. Remove default touch actions for elements such as links and buttons. Fix focus styling by removing the outline for input elements. Remove spin buttons from numeric inputs in WebKit and Gecko browsers. Ensure tables do not have double borders by enabling border-collapse. Remove the default dropdown arrow from select elements in IE.
*, ::before, ::after: box-sizing: border-box
a, area, button, input, label, select, summary, textarea, [tabindex]: touch-action: manipulation
button:focus, input:focus, select:focus, textarea:focus, [contenteditable]:focus: outline: 0
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button: -webkit-appearance: none
input[type="number"]: -moz-appearance: textfield
table: border-collapse: collapse
::-ms-expand: display: none
object

%flex

/phlo/libs/CSS/flex.phlo
version 1.0
creator q-ai.nl
description Single Page App CSS flex(box) boilerplate
view

style

line 5
Sets flexible layout rules with various flex properties, such as display, flex-wrap, gap, and flex-direction, including responsive adjustments for larger screens.
.flex: display: flex
.flex.auto > *: flex: auto
.flex.gap: gap: 1rem
.flex.row: flex-direction: column
.flex.wrap: flex-wrap: wrap
.grow: flex-grow: 1

@media (min-width:768px){
	.flex.col: flex-direction: column
	.flex.col.row: flex-direction: row
}
object

%grid

/phlo/libs/CSS/grid.phlo
version 1.0
creator q-ai.nl
description Single Page App CSS grid boilerplate
view

style

line 5
This code defines a grid layout that automatically adjusts the number of columns based on the available width, with a minimum of 300px per column.
.grid {
	display: grid
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))
}

DB

object

%DB

/phlo/libs/DB/DB.phlo
version 1.0
creator q-ai.nl
description Database engine class
type abstract class
prop

%DB -> PDO

line 6
Displays an error message if no PDO connection is defined.
error('No PDO connector defined')
prop

%DB -> fieldQuotes

line 7
Returns a boolean value indicating whether quotes (quotation marks) are around the data, possibly to check if the field is already quoted.
bt
method

%DB -> load (string $table, string $columns = '*', string $where = void, string $joins = void, string $group = void, string $limit = null, string $order = void, ...$args)

line 9
Constructs an SQL SELECT statement dynamically based on provided parameters and additional arguments, including automatic WHERE clause generation from key-value pairs if not specified.
!$where && $args && $where = loop(array_keys($args), fn($column) => "$table.$column=?", ' AND ')
$joins && $joins = " $joins"
$where && $where = " WHERE $where"
$group && $group = " GROUP BY $group"
$order && $order = " ORDER BY $order"
$limit && $limit = " LIMIT $limit"
$query = "SELECT $columns FROM $table$joins$where$group$order$limit"
return $this->query($query, ...array_values($args))
method

%DB -> query ($query, ...$args)

line 20
Executes a database query with support for prepared statements for input parameters, and includes debug functionality that displays the query type, tablename, and optionally the WHERE clause along with the number of rows returned. In case of an error, a detailed error message including trace is generated.
try {
	if (!$args) $stmt = $this->PDO->query($query)
	else {
		$stmt = $this->PDO->prepare($query)
		$stmt->execute($args)
	}
	if (debug){
		$match = regex('/\b(UPDATE|INSERT INTO|DELETE FROM|FROM)\b\s+([`"\[]?\w+[`"\]]?)/i', strtr($query, [$this->fieldQuotes => void]))
		$where = strtr(regex('/\bWHERE (\b.+)/is', $query)[1] ?? void, [' ORDER BY' => void])
		$match && debug("Q: $match[1] $match[2]".strtr(rtrim(" $where "), [dq => void])." (".$stmt->rowCount().")")
	}
}
catch (PDOException $e){
	error('Database error'.(debug ? colon.lf.$query.lf.lf.$e->getMessage().lf.lf.loop(array_slice(explode(lf, $e->getTraceAsString()), 2, -2), fn($line) => last($match = regex('/([^\/]+\.php)\(([0-9]+)\): (.*)/', $line), "$match[3] $match[1]:$match[2]"), lf) : void))
}
return $stmt
method

%DB -> column (...$args)

line 39
Loads the data with the specified arguments and returns a list of one column from all rows.
$this->load(...$args)->fetchAll(PDO::FETCH_COLUMN)
method

%DB -> item (...$args)

line 40
Retrieves a loaded database data and returns the first column of the first row, unless it does not exist, then null.
$this->load(...$args)->fetch(PDO::FETCH_COLUMN) ?: null
method

%DB -> pair (...$args)

line 41
Retrieves data and returns it as a key-value pair array.
$this->load(...$args)->fetchAll(PDO::FETCH_KEY_PAIR)
method

%DB -> group (...$args)

line 42
This code loads data with the given arguments, retrieves all results, and groups them based on a column or field, with each group being converted into objects of the 'obj' class.
$this->load(...$args)->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_CLASS, 'obj')
method

%DB -> records (...$args)

line 43
Loads data with the specified arguments and retrieves all results, converting each row into an object of the specified class, with unique results based on the first column.
$this->load(...$args)->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_UNIQUE, 'obj')
method

%DB -> record (...$args)

line 44
Returns an object based on loaded data, or null if there is no result.
$this->load(...$args)->fetchObject('obj') ?: null
method

%DB -> create (string $table, ...$data)

line 46
Execute an insert query where, if the 'ignore' value in the data is true, the 'IGNORE' option is added. Properly construct column names and corresponding values, fill the values with placeholders, and replace them with the actual data. Execute the query and return the last inserted ID or an optional ID.
if ($ignore = $data['ignore'] ?? false) unset($data['ignore'])
$columns = $this->fieldQuotes.implode($this->fieldQuotes.comma.$this->fieldQuotes, array_keys($data)).$this->fieldQuotes
$values = implode(comma, array_fill(0, count($data), qm))
$query = "INSERT".($ignore ? ' IGNORE' : void)." INTO $table ($columns) VALUES ($values)"
$this->query($query, ...array_values(loop($data, fn($value) => is_a($value, 'obj') ? $value->id : $value)))
return $this->PDO->lastInsertId() ?: ($data['id'] ?? null)
method

%DB -> change (string $table, string $where, ...$data)

line 55
This function dynamically constructs an SQL UPDATE statement using the data from `$data`. It counts the number of placeholders in `$where` to determine which data is for the WHERE condition and which is for the SET clause. Then, the column names and values are separated and ordered, and the query is executed with the correct parameters, after which the number of affected rows is returned.
$whereCount = substr_count($where, qm)
$updates = isset($data['updates']) ? $data['updates'] : void
unset($data['updates'])
$updates .= (($wheres = array_slice(array_keys($data), $whereCount)) && $updates ? comma : void).loop($wheres, fn($key) => $key.'=?', comma)
$query = "UPDATE $table SET $updates WHERE $where"
$args = array_values([...array_slice($data, $whereCount), ...array_slice($data, 0, $whereCount)])
return $this->query($query, ...$args)->rowCount()
method

%DB -> delete (string $table, string $where, ...$args)

line 65
Executes a DELETE query on a specified table with a specific WHERE condition and returns the number of rows deleted.
$this->query("DELETE FROM $table WHERE $where", ...$args)->rowCount()
object

%MySQL

/phlo/libs/DB/MySQL.phlo
version 1.0
creator q-ai.nl
description MySQL handler via DB class
extends DB
requires @DB creds:mysql
prop

%MySQL -> PDO

line 8
Creates a new PDO connection to the MySQL database, with the connection details retrieved from a credentials object.
new PDO('mysql:host='.%creds->mysql->host.';dbname='.%creds->mysql->database, %creds->mysql->user, %creds->mysql->password)
object

%PostgreSQL

/phlo/libs/DB/PostgreSQL.phlo
version 1.0
creator q-ai.nl
description PostgreSQL library
extends DB
requires @DB creds:postgresql
prop

%PostgreSQL -> PDO

line 8
Creates a new PDO connection to a PostgreSQL database using configured login credentials.
new PDO('pgsql:host='.%creds->postgresql->host.';dbname='.%creds->postgresql->database, %creds->postgresql->user, %creds->postgresql->password)
prop

%PostgreSQL -> fieldQuotes

line 9
This code defines a function that likely applies a textual replacement or modification to the input, possibly related to field or table name quotes for PostgreSQL, to prevent SQL injections and syntax errors.
dq
object

%Qdrant

/phlo/libs/DB/Qdrant.phlo
version 1.0
creator q-ai.nl
description Embeddings library with Qdrant
method

%Qdrant -> get (string $input)

line 5
Stores an embedding in APCu-cache, with a key based on the input, and retrieves it or generates and caches it for 28 days.
apcu('embedding/'.token(input: $input), fn($input) => %OpenAI->embedding($input), 86400 * 28)
method

%Qdrant -> collections

line 7
Retrieves the names from the collections and returns them as an array.
array_column($this->request('collections')->result->collections, 'name')
method

%Qdrant -> create ($collection, $size = 1536, $distance = 'Cosine')

line 8
Creates a new collection with the specified name, vector size, and distance metric, and checks if the creation was successful.
$this->request("collections/$collection", PUT: arr(vectors: arr(size: $size, distance: $distance)))->status === 'ok'
method

%Qdrant -> upsert ($collection, $id, $input, ...$payload)

line 9
Performs an HTTP PUT request to the endpoint for updating or adding a point in a collection. It sends an array with point data, including ID, vector, and optionally (payload). The result is the operation ID of the performed operation.
$this->request("collections/$collection/points", PUT: arr(points: [arr(id: $id, vector: $this->get($input), payload: $payload ?: null)]))->result->operation_id
method

%Qdrant -> delete ($collection, ...$ids)

line 10
Sends a POST request to the API to delete specific points from a collection by specifying their IDs, with the endpoint 'collections/{collection}/points/delete'.
$this->request("collections/$collection/points/delete", POST: arr(points: $ids))->result
method

%Qdrant -> search ($collection, $input = null, $top = 100)

line 11
Performs a search operation in a collection by sending points to the API, with default or provided input. Results are filtered and returned with specific IDs and payload data, with missing input filled with zeros in a vector of 1536 elements.
create($this->request("collections/$collection/points/search", POST: arr(vector: is_null($input) ? array_fill(0, 1536, 0) : $this->get($input), top: $top, with_payload: true))->result, fn($record) => $record->id, fn($record) => last($record = array_merge(get_object_vars($record), get_object_vars($record->payload)), obj(...array_filter($record, fn($key) => $key !== 'payload', ARRAY_FILTER_USE_KEY))))
method

%Qdrant -> drop ($collection)

line 12
Deletes a collection with the specified name by sending a DELETE request to the server and returning the result.
$this->request("collections/$collection", DELETE: true)->result
method

%Qdrant -> request ($uri, ...$data)

line 14
Sends an HTTP request to a Qdrant server with the specified URI and data, including an API key if necessary, and then decodes the JSON response into an associative array.
json_decode(HTTP(%creds->qdrant->server.$uri, %creds->qdrant->key ? ['api-key: '.%creds->qdrant->key] : [], true, ...$data))
object

%SQLite

/phlo/libs/DB/SQLite.phlo
version 1.0
creator q-ai.nl
description SQLite library
extends DB
requires @DB
method

%SQLite -> controller

line 8
Creates a connection to an SQLite database with the filename specified in `$file`.
andle => "SQLite/$file"
method

%SQLite -> __construct (private string $file)

line 9
This is the constructor of the class, which receives a path of type string as a parameter and is likely used to store or initialize the path or name of the SQLite database.
prop

%SQLite -> PDO

line 11
Creates a new PDO instance with an SQLite database, using the database filename from a property of the object.
new PDO('sqlite:'.$this->file)

DOM

object

%CSS_var

/phlo/libs/DOM/CSS.var.phlo
version 1.0
creator q-ai.nl
description CSS variable name proxy via app.var
view

script

line 5
This code defines a property that provides access to CSS variables on the document via a proxy. When read, the value of the CSS variable is retrieved, and when written, it is modified.
Object.defineProperty(app, 'var', {get(){return new Proxy({}, {get(_, key){return getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim()}, set(_, key, value){ return document.documentElement.style.setProperty(`--${key}`, value)}})}, configurable: true})
object

%datatags

/phlo/libs/DOM/datatags.phlo
version 1.0
creator q-ai.nl
description Single Page App datatag plugin
view

script

line 5
This code responds to click events on elements with specific data-attributes and determines the HTTP method and URI based on that. Then, the appropriate API call is made via an app function, with optional data passed along.
on('click', '[data-get], [data-post], [data-put], [data-patch], [data-delete]', (el, e) => {
	if (el.dataset.confirm) return
	e.preventDefault()
	let method, uri, data = null
	if ((uri = el.dataset.get) !== undefined) method = 'get'
	else if ((uri = el.dataset.delete) !== undefined) method = 'delete'
	else [data = {}, Object.keys(el.dataset).forEach((key) => key === 'post' || key === 'put' || key === 'patch' ? [method = key, uri = el.dataset[key]] : data[key] = el.dataset[key])]
	app[method](uri, data)
})
object

%dialog

/phlo/libs/DOM/dialog.phlo
version 1.0
creator q-ai.nl
description Single Page App dialog library
view

script

line 5
This code defines a set of dialog functions (alert, confirm, prompt) that use a dynamically created HTML dialog. The 'phlo.dialog' function creates a dialog window based on the type and displays it, waits for the user, and then returns the appropriate value. Additionally, a click event listener is created that, upon detecting a 'data-confirm' attribute, shows a confirmation dialog and takes further actions based on the response.
app.alert = (msg) => phlo.dialog('alert', msg)
app.confirm = (msg) => phlo.dialog('confirm', msg)
app.prompt = (msg, def) => phlo.dialog('prompt', msg, def)

phlo.dialog = async (type, message, defaultValue = '') => new Promise((resolve) => {
	app.mod.append('body', ['<dialog>', '<form method="dialog">', `<p>${message}</p>`, type === 'prompt' ? `<input name="value" value="${defaultValue}">` : '', '<menu>', '<button value="1" autofocus>OK</button>', type !== 'alert' ? ' <button value="0">Cancel</button>' : '', '</menu>', '</form>', '</dialog>'].join(''))
	const dialog = obj('dialog')
	dialog.showModal()
	dialog.addEventListener('close', () => {
		const value = dialog.returnValue
		const input = dialog.querySelector('input')
		dialog.remove()
		if (type === 'alert') return resolve()
		if (type === 'confirm') return resolve(value === '1')
		if (type === 'prompt') return resolve(value === '1' ? input.value : null)
	})
})

on('click', '[data-confirm]', async (el, e) => {
	e.preventDefault()
	if (!await app.confirm(el.dataset.confirm)) return
	delete el.dataset.confirm
	app.update()
	el.click()
})
object

%exists

/phlo/libs/DOM/exists.phlo
version 1.0
creator q-ai.nl
description onExist function for initialising DOM elements in a dynamic SPA environment
view

script

line 5
This code defines a functionality to detect whether elements exist: it maintains a list of elements and their associated callbacks, and executes these callbacks when the elements have not been previously registered as existing, using a WeakMap to keep track of already registered elements.
phlo.exist = []
phlo.existing = new WeakMap

const onExist = (els, cb) => phlo.exist.push({els, cb})

app.updates.push(() => {
	const existing = []
	phlo.exist.forEach(item => objects(item.els).forEach(el => phlo.existing.has(el) || existing.push({el, cb: item.cb})))
	existing.forEach(item => [phlo.existing.has(item.el) || phlo.existing.set(item.el, 'exist'), item.cb(item.el)])
})
object

%form

/phlo/libs/DOM/form.phlo
version 1.0
creator q-ai.nl
description Single Page App form handler and input state saver
view

script

line 5
This code contains a function that submits form data via an AJAX call based on the method name, and an event listener that synchronizes the form values with attributes and saves the internal state upon input or change. Additionally, it provides a custom submit handler for forms with the class 'async' to prevent default behavior and send the data via the function.
const submitForm = (form) => {
	const method = (form.attributes.method?.value ?? 'GET').toLowerCase()
	app[method](new URL(form.action).pathname.substr(1), new FormData(form))
}
on('input change', 'input, select, textarea', input => {
	if (input.tagName === 'SELECT') input.querySelectorAll('option').forEach((option, index) => option.selected ? option.setAttribute('selected', '') : option.removeAttribute('selected'))
	if (input.type === 'checkbox') input.checked ? input.setAttribute('checked', '') : input.removeAttribute('checked')
	if (input.type === 'text' && input.value !== input.getAttribute('value')) input.setAttribute('value', input.value)
	if (input.type === 'textarea' && input.value !== input.innerHTML) input.innerHTML = input.value
	phlo.state.save()
	return false
})
on('submit', 'form.async', (form, e) => {
	e.preventDefault()
	submitForm(form)
})
object

%image_resizer

/phlo/libs/DOM/image.resizer.phlo
version 1.0
creator q-ai.nl
description Clientside file upload image resizer
view

script

line 5
Loads an image, adjusts the dimensions based on maximum width and height while maintaining aspect ratio, and generates a resized datalink via a callback.
const imageResizer = (file, maxWidth, maxHeight, cb, quality = .8) => {
	const img = new Image
	img.onload = () => {
		let width = img.width, height = img.height
		const aspectRatio = width / height
		if (width > maxWidth || height > maxHeight){
			if (width > height){
				width = maxWidth
				height = Math.round(maxWidth / aspectRatio)
			}
			else {
				height = maxHeight
				width = Math.round(maxHeight * aspectRatio)
			}
		}
		const canvas = document.createElement('canvas')
		canvas.width = width
		canvas.height = height
		canvas.getContext('2d').drawImage(img, 0, 0, width, height)
		cb(canvas.toDataURL(file.type, quality))
	}
	img.src = URL.createObjectURL(file)
}
object
/phlo/libs/DOM/link.phlo
version 1.0
creator q-ai.nl
description Single Page App link.async handler
view

script

line 5
In the situation where clicking on a link with the class 'async', depending on keystrokes or confirmation status, blocks the default navigation and an asynchronous fetch action is performed.
on('click', 'a.async', (a, e) => e.ctrlKey || e.shiftKey || e.metaKey || a.dataset.confirm || [e.preventDefault(), app.get(a.attributes.href.value.substr(1))])
object

%markdown

/phlo/libs/DOM/markdown.phlo
version 1.0
creator q-ai.nl
description Clientside markdown parser
view

script

line 5
Parse transformation for Markdown text to HTML with support for GFM functionality. Markers are recognized and converted, such as headings, lists, tables, code blocks, blockquotes, and inline elements. Sanitization is applied to hyperlinks, images, and text decorations, and HTML is embedded securely. References and ID generation logic are also used for headings and tables.
function parse_markdown(md, opts = {}){
  const o = {
    gfm: opts.gfm !== false,
    breaks: !!opts.breaks,
    headerIds: opts.headerIds !== false,
    headerPrefix: opts.headerPrefix || '',
    smartypants: !!opts.smartypants
  }
  const unnull = x => (x == null ? '' : String(x))
  let src = unnull(md).replace(/\r\n?/g, "\n")
  const escHtml = s => s.replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))
  const trimEndNL = s => s.replace(/\s+$/,'')
  const isBlank = s => /^\s*$/.test(s)
  const slugmap = new Map()
  const slug = (t) => {
    let s = t.toLowerCase().replace(/<\/?[^>]+>/g, '').replace(/[^\p{L}\p{N}\- _]+/gu, '').trim().replace(/[\s_]+/g, '-')
    const base = o.headerPrefix + s
    let k = base, i = 1
    while (slugmap.has(k)) k = `${base}-${++i}`
    slugmap.set(k, true)
    return k
  }
  const smart = s => {
    if (!o.smartypants) return s
    return s.replace(/---/g, "—").replace(/--/g, "–").replace(/(^|[\s"(\[])(?=')/g, "$1‘").replace(/'/g, "’").replace(/(^|[\s(\[])(?=")/g, "$1“").replace(/"/g, "”").replace(/\.{3}/g, "…")
  }
  const refs = Object.create(null)
  src = src.replace(
    /^ {0,3}\[([^\]]+)\]:\s*<?([^\s>]+)>?(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*$/gm,
    (_, label, url, t1, t2, t3) => {
      const key = label.trim().replace(/\s+/g, ' ').toLowerCase()
      if (!refs[key]) refs[key] = { href: url, title: t1 || t2 || t3 || '' }
      return ''
    }
  )
  const tokens = []
  const lines = src.split("\n")
  function takeWhile(start, pred){
    let end = start
    while (end < lines.length && pred(lines[end], end)) end++
    return { start, end }
  }
  function pushParagraph(buf){
    const text = buf.join("\n").trimEnd()
    if (text) tokens.push({ type: "paragraph", text })
    buf.length = 0
  }
  function parseBlock(start = 0, end = lines.length){
    const para = []
    let l = start
    while (l < end){
      const line = lines[l]
      if (isBlank(line)){ pushParagraph(para); l++; continue; }
      let m = line.match(/^ {0,3}(`{3,}|~{3,})([^\n]*)$/)
      if (m){
        pushParagraph(para)
        const fenceLen = m[1].length
        const info = (m[2] || '').trim()
        let body = []
        l++
        while (l < end){
          const s = lines[l]
          const close = s.match(new RegExp(`^ {0,3}${m[1][0]}{${fenceLen},}\\s*$`))
          if (close){ l++; break; }
          body.push(s)
          l++
        }
        tokens.push({ type: "code", lang: info.split(/\s+/)[0] || '', text: trimEndNL(body.join("\n")) })
        continue
      }
      if (/^(?: {4}|\t)/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^(?: {4}|\t)/.test(s) || isBlank(s))
        const block = lines.slice(l, j).map(s => s.replace(/^(?: {4}|\t)/, '')).join("\n")
        tokens.push({ type: "code", lang: '', text: trimEndNL(block) })
        l = j; continue
      }
      if (/^ {0,3}<(?:!--|\/?(?:html|head|body|pre|script|style|table|thead|tbody|tfoot|tr|td|th|div|p|h[1-6]|blockquote|ul|ol|li|section|article|aside|details|summary|figure|figcaption)\b)/i.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, (s, idx) => !(idx > l && isBlank(lines[idx-1]) && isBlank(s)))
        const html = lines.slice(l, j).join("\n")
        tokens.push({ type: "html", text: html })
        l = j; continue
      }
      if (/^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(line)){
        pushParagraph(para)
        tokens.push({ type: "hr" })
        l++; continue
      }
      m = line.match(/^ {0,3}(#{1,6})[ \t]*([^#\n]*?)[ \t#]*$/)
      if (m){
        pushParagraph(para)
        tokens.push({ type: "heading", depth: m[1].length, text: m[2].trim() })
        l++; continue
      }
      if (l + 1 < end && /^[^\s].*$/.test(line) && /^ {0,3}(=+|-+)\s*$/.test(lines[l + 1])){
        pushParagraph(para)
        const depth = lines[l + 1].trim().startsWith("=") ? 1 : 2
        tokens.push({ type: "heading", depth, text: line.trim() })
        l += 2; continue
      }
      if (/^ {0,3}>\s?/.test(line)){
        pushParagraph(para)
        const { end: j } = takeWhile(l, s => /^ {0,3}>\s?/.test(s) || isBlank(s))
        const inner = lines.slice(l, j).map(s => s.replace(/^ {0,3}>\s?/, '')).join("\n")
        const sub = parse_markdown(inner, { ...o })
        tokens.push({ type: "blockquote", html: sub })
        l = j; continue
      }
      m = line.match(/^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/)
      if (m){
        pushParagraph(para)
        const bulletRe = /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/
        const { end: j } = takeWhile(l, (s, idx) =>
          bulletRe.test(s) ||
          (/^(?: {4}|\t)/.test(s)) ||
          (!isBlank(s) && idx > l && !/^(?: {0,3}(?:[*+-]|\d{1,9}[.)])\s+)/.test(s))
        )
        const block = lines.slice(l, j)
        const ordered = /^\d/.test(m[1])
        const items = []
        let cur = []
        for (let k = 0; k < block.length; k++){
          const ln = block[k]
          const head = ln.match(bulletRe)
          if (head){
            if (cur.length) items.push(cur), cur = []
            cur.push(ln.replace(bulletRe, ''))
          } else {
            cur.push(ln.replace(/^(?: {4}|\t)/, ''))
          }
        }
        if (cur.length) items.push(cur)
        const parsedItems = items.map(linesArr => {
          let raw = linesArr.join("\n").replace(/\n\s+$/,'')
          let checked = null
          if (o.gfm){
            const t = raw.match(/^\[([ xX])\][ \t]+/)
            if (t){ checked = t[1].toLowerCase() === 'x'; raw = raw.replace(/^\[[ xX]\][ \t]+/, ''); }
          }
          const html = parse_markdown(raw, o)
          return { html, checked }
        })
        tokens.push({ type: "list", ordered, items: parsedItems })
        l = j; continue
      }
      if (o.gfm){
        const hdr = line
        const alignLn = lines[l + 1] || ''
        if (/\|/.test(hdr) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(alignLn)){
          pushParagraph(para)
          const aligns = alignLn
            .trim().replace(/^(\|)|(\|)$/g,'')
            .split("|").map(s => s.trim()).map(s => s.startsWith(":-") && s.endsWith("-:") ? "center" : s.endsWith("-:") ? "right" : s.startsWith(":-") ? "left" : null)
          const headerCells = hdr.trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim())
          l += 2
          const rows = []
          while (l < end && /\|/.test(lines[l]) && !isBlank(lines[l])){
            rows.push(lines[l].trim().replace(/^(\|)|(\|)$/g,'').split("|").map(s => s.trim()))
            l++
          }
          tokens.push({ type: "table", header: headerCells, aligns, rows })
          continue
        }
      }
      para.push(line)
      const next = lines[l + 1] || ''
      const endPara =
        isBlank(next) ||
        /^ {0,3}(?:`{3,}|~{3,})/.test(next) ||
        /^(?: {4}|\t)/.test(next) ||
        /^ {0,3}((?:[*+-])|\d{1,9}[.)])\s+/.test(next) ||
        /^ {0,3}(#{1,6})/.test(next) ||
        /^ {0,3}>\s?/.test(next) ||
        /^ {0,3}(?:-+\s*|-{3,}|_{3,}|\*{3,})\s*$/.test(next) ||
        (o.gfm && /\|/.test(next) && /^ {0,3}\|? *:?-+:? *(?:\| *:?-+:? *)*\|? *$/.test(lines[l + 2] || ''))
      if (endPara) pushParagraph(para)
      l++
    }
    pushParagraph(para)
  }
  parseBlock(0, lines.length)
  function renderInline(s){
    if (!s) return ''
    s = s.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1/g, (_, ticks, code) => `<code>${escHtml(code)}</code>`)
    s = s.replace(/!\[([^\]]*)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, alt, url, t1, t2, t3) => `<img src="${escHtml(url)}" alt="${escHtml(alt)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>`)
    s = s.replace(/!\[([^\]]*)\]\[([^\]]*)\]/g, (_, alt, id) => {
      const ref = refs[(id || alt).trim().replace(/\s+/g,' ').toLowerCase()]
      return ref ? `<img src="${escHtml(ref.href)}" alt="${escHtml(alt)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>` : _
    })
    s = s.replace(/\[([^\]]+)\]\(\s*<?([^\s)<>]+)>?\s*(?:(?:"([^"]*)"|'([^']*)'|\(([^)]+)\)))?\s*\)/g,
      (_, text, url, t1, t2, t3) => `<a href="${escHtml(url)}"${t1||t2||t3?` title="${escHtml(t1||t2||t3)}"`:''}>${text}</a>`)
    s = s.replace(/\[([^\]]+)\]\s*\[([^\]]*)\]/g, (_, text, id) => {
      const key = (id || text).trim().replace(/\s+/g,' ').toLowerCase()
      const ref = refs[key]
      return ref ? `<a href="${escHtml(ref.href)}"${ref.title?` title="${escHtml(ref.title)}"`:''}>${text}</a>` : _
    })
    s = s.replace(/<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^ <>"']+)>/g, (_, url) => `<a href="${escHtml(url)}">${escHtml(url)}</a>`)
    s = s.replace(/<([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, (_, mail) => `<a href="mailto:${escHtml(mail)}">${escHtml(mail)}</a>`)
    if (o.gfm){
      s = s.replace(/(?:(?<=\s)|^)(https?:\/\/[^\s<]+)(?=\s|$)/g, '<a href="$1">$1</a>')
      s = s.replace(/(?:(?<=\s)|^)(www\.[^\s<]+)(?=\s|$)/g, '<a href="http://$1">$1</a>')
    }
    s = s.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>').replace(/__([\s\S]+?)__/g, '<strong>$1</strong>')
    s = s.replace(/\*([^*\n]+?)\*/g, '<em>$1</em>').replace(/_([^_\n]+?)_/g, '<em>$1</em>')
    if (o.gfm) s = s.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>')
    s = s.replace(/ {2,}\n/g, "<br>\n")
    if (o.breaks) s = s.replace(/\n/g, "<br>\n")
    s = s.replace(/&(?!#?\w+;)/g, "&amp;").replace(/<(?!\/?[A-Za-z][^>]*>)/g, "&lt;")
    return smart(s)
  }
  let out = ''
  for (const t of tokens){
    switch (t.type){
      case "paragraph":
        out += `<p>${renderInline(t.text)}</p>\n`
        break
      case "heading": {
        const text = renderInline(t.text)
        const id = o.headerIds ? slug(text.replace(/<[^>]+>/g, '')) : null
        out += id ? `<h${t.depth} id="${id}">${text}</h${t.depth}>\n` : `<h${t.depth}>${text}</h${t.depth}>\n`
        break
      }
      case "code": {
        const cls = t.lang ? ` class="language-${escHtml(t.lang)}"` : ''
        out += `<pre><code${cls}>${escHtml(t.text)}</code></pre>\n`
        break
      }
      case "blockquote":
        out += `<blockquote>\n${t.html.trim()}\n</blockquote>\n`
        break
      case "list": {
        const tag = t.ordered ? "ol" : "ul"
        out += `<${tag}>\n`
        for (const it of t.items){
          const task = it.checked === null ? '' : `<input ${it.checked ? 'checked="" ' : ''}disabled="" type="checkbox"> `
          const body = it.html.trim().replace(/^<p>/, task + "<p>")
          out += `<li>${body}</li>\n`
        }
        out += `</${tag}>\n`
        break
      }
      case "table": {
        const ths = t.header.map((h, i) => {
          const a = t.aligns[i]
          return a ? `<th align="${a}">${renderInline(h)}</th>` : `<th>${renderInline(h)}</th>`
        }).join("\n")
        let body = ''
        for (const row of t.rows){
          const tds = row.map((cell, i) => {
            const a = t.aligns[i]
            return a ? `<td align="${a}">${renderInline(cell)}</td>` : `<td>${renderInline(cell)}</td>`
          }).join("\n")
          body += `<tr>\n${tds}\n</tr>\n`
        }
        out += `<table>\n<thead>\n<tr>\n${ths}\n</tr>\n</thead>\n` + (body ? `<tbody>\n${body}</tbody>\n` : '') + `</table>\n`
        break
      }
      case "hr":
        out += "<hr>\n"
        break
      case "html":
        out += t.text + "\n"
        break
    }
  }
  return out.trim()
}
object

%shorthands

/phlo/libs/DOM/shorthands.phlo
version 1.0
creator q-ai.nl
description onChange, onClick and onInput event shorthands
view

script

line 5
These functions are short helper functions that add event listeners for 'change', 'click', and 'input' events, with fewer arguments needed.
function onChange(els, cb){ on('change', els, cb) }
function onClick(els, cb){ on('click', els, cb) }
function onInput(els, cb){ on('input', els, cb) }
object

%store

/phlo/libs/DOM/store.phlo
version 1.1
creator q-ai.nl
description Statefull/binding engine
view

script

line 5
This code implements a reactive data storage and coupling system with observation and automatic updates. It includes methods for retrieving and setting data via paths, managing signals, handling dependencies between calculations, and updating DOM elements based on the store. Additionally, it uses proxies for dynamic access and modification of data, and ensures synchronization between DOM and store via data-attributes and event handlers. The structure supports automatic evaluation of calculations and efficient re-calculation upon changes.
phlo.store = {
	signals: {},
	listeners: {},
	calcs: {},
	calcDeps: {},
	calcVals: {},
	calcTick: false,
	split: (path) => path.replace(/\]/g, '').split(/\.|\[/),
	get(path){
		if (!path) return undefined
		let ctx = phlo.store.signals
		const keys = phlo.store.split(path)
		for (let i = 0; i < keys.length; i++){
			if (ctx == null) return undefined
			ctx = ctx[keys[i]]
		}
		return ctx
	},
	setPath(path, value){
		let keys = phlo.store.split(path)
		let ctx = phlo.store.signals
		while (keys.length > 1){
			const k = keys.shift()
			ctx[k] ??= isNaN(keys[0]) ? {} : []
			ctx = ctx[k]
		}
		const k = keys[0]
		const old = ctx[k]
		if (old === value) return false
		ctx[k] = value
		return true
	},
	set(path, value){
		if (!phlo.store.setPath(path, value)) return
		phlo.store.notify(path, phlo.store.get(path))
		phlo.store.recalc(path)
		phlo.store.schedule()
	},
	on(path, cb){ (phlo.store.listeners[path] ??= new Set).add(cb) },
	off(path, cb){ phlo.store.listeners[path] && phlo.store.listeners[path].delete(cb) },
	reset(){
		phlo.store.signals = {}
		phlo.store.listeners = {}
		phlo.store.calcs = {}
		phlo.store.calcDeps = {}
		phlo.store.calcVals = {}
		phlo.store.calcTick = false
	},
	signal(path, initial){
		if (phlo.store.get(path) === undefined) phlo.store.set(path, initial)
		return { subscribe: (cb) => phlo.store.on(path, cb), unsubscribe: (cb) => phlo.store.off(path, cb) }
	},
	notify(path, val){
		const set = phlo.store.listeners[path]
		if (set) set.forEach(cb => cb(val))
	},
	match(dep, changed){
		if (!dep) return false
		if (dep === changed) return true
		return changed.startsWith(dep + '.') || changed.startsWith(dep + '[') || dep.startsWith(changed + '.') || dep.startsWith(changed + '[')
	},
	depsReady(list){
		const arr = Array.isArray(list) ? list : (list ? [list] : [])
		return arr.every(d => phlo.store.get(d) !== undefined)
	},
	evalCalc(name){
		const fn = phlo.store.calcs[name]
		if (!fn) return
		let deps = []
		let val
		try {
			const out = fn()
			if (Array.isArray(out) && out.length === 2) deps = out[0], val = out[1]
			else val = out
		}
		catch(e){
			deps = []
			val = undefined
		}
		const list = Array.isArray(deps) ? deps : (deps ? [deps] : [])
		phlo.store.calcDeps[name] = list
		if (!phlo.store.depsReady(list)) return
		const old = phlo.store.calcVals[name]
		if (old !== val){
			phlo.store.calcVals[name] = val
			const p = `calc.${name}`
			phlo.store.setPath(p, val)
			phlo.store.notify(p, val)
		}
	},
	recalc(changed){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++){
			const name = names[i]
			const deps = phlo.store.calcDeps[name] || []
			for (let j = 0; j < deps.length; j++){
				if (phlo.store.match(deps[j], changed)){
					phlo.store.evalCalc(name)
					break
				}
			}
		}
	},
	recalcAll(){
		const names = Object.keys(phlo.store.calcs)
		for (let i = 0; i < names.length; i++) phlo.store.evalCalc(names[i])
	},
	schedule(){
		if (phlo.store.calcTick) return
		phlo.store.calcTick = true
		setTimeout(() => {
			phlo.store.calcTick = false
			phlo.store.recalcAll()
		})
	},
	proxy(base){
		return new Proxy({}, {
			get(t, k){
				if (typeof k === 'symbol') return undefined
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				const path = base + seg
				const v = phlo.store.get(path)
				if (v !== undefined && (typeof v !== 'object' || v === null)) return v
				return phlo.store.proxy(path)
			},
			set(t, k, v){
				const seg = /^\d+$/.test(k) ? `[${k}]` : (base ? `.${k}` : String(k))
				phlo.store.set(base + seg, v)
				return true
			},
			has(t, k){ return phlo.store.get(base + (base ? '.' : '') + String(k)) !== undefined },
			ownKeys(){ return Object.keys(phlo.store.get(base) || {}) },
			getOwnPropertyDescriptor(){ return { enumerable: true, configurable: true } }
		})
	}
}

app.store = phlo.store.proxy('')

app.mod.store = (key, value) => {
	const cur = phlo.store.get(key)
	if (JSON.stringify(cur) === JSON.stringify(value)) return
	const walk = (base, obj) => {
		if (typeof obj !== 'object' || obj === null) return phlo.store.set(base, obj)
		Object.entries(obj).forEach(([k, v]) => walk(isNaN(k) ? `${base}.${k}` : `${base}[${k}]`, v))
	}
	walk(key, value)
}

phlo.calc = new Proxy({}, {
	set(t, k, fn){
		if (typeof fn !== 'function') return false
		phlo.store.calcs[k] = fn
		phlo.store.evalCalc(k)
		setTimeout(() => phlo.store.evalCalc(k))
		return true
	},
	get(t, k){ return phlo.store.calcs[k] },
	has(t, k){ return k in phlo.store.calcs },
	deleteProperty(t, k){
		delete phlo.store.calcs[k]
		delete phlo.store.calcDeps[k]
		delete phlo.store.calcVals[k]
		return true
	}
})

app.calc = new Proxy({}, {
	get(t, k){ return phlo.store.calcVals[k] },
	has(t, k){ return k in phlo.store.calcVals },
	ownKeys(){ return Object.keys(phlo.store.calcVals) },
	getOwnPropertyDescriptor(){ return { enumerable: true, configurable: true } }
})

onExist('[data-bind]', (el) => {
	const key = el.dataset.bind
	const isCalc = key.startsWith('calc.')
	const isInput = el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA'
	const fromDom = isInput ? el.value : el.textContent
	const fromStore = phlo.store.get(key)
	const domNonEmpty = (fromDom ?? '').trim() !== ''
	const storeEmpty = fromStore === undefined || (typeof fromStore === 'string' && fromStore.trim() === '')
	const domLeads = !isCalc && domNonEmpty && storeEmpty
	const S = (v) => v == null ? '' : (typeof v === 'object' ? '' : String(v))
	const apply = (v) => {
		const s = S(v)
		if (isInput) el.value = s
		else el.textContent = s
	}
	phlo.store.on(key, apply)
	if (domLeads) phlo.store.set(key, fromDom)
	const initial = domLeads ? fromDom : fromStore
	apply(initial)
	if (!isCalc && isInput) el.oninput = (e) => phlo.store.set(key, e.target.value)
})

onExist('[data-bind-attr]', (el) => {
	const spec = el.getAttribute('data-bind-attr')
	if (!spec) return
	const BOOL = new Set(['disabled','checked','hidden','required','readonly','selected','autofocus','multiple'])
	let meta = phlo.existing.get(el)
	if (!meta || typeof meta !== 'object'){
		meta = { exist: true }
		phlo.existing.set(el, meta)
	}
	meta.attr || (meta.attr = {})
	meta.attr.cls || (meta.attr.cls = [])
	spec.split(/\s*,\s*/).filter(Boolean).forEach(pair => {
		const m = pair.match(/^\s*([^:]+)\s*:\s*(.+)\s*$/)
		if (!m) return
		const name = m[1]
		const path = m[2]
		const isCalc = path.startsWith('calc.')
		const isInputVal = name === 'value' && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')
		const domVal =
			name === 'text' ? el.textContent :
			name === 'html' ? el.innerHTML :
			name === 'value' ? el.value :
			(name === 'class' ? null : el.getAttribute(name))
		const fromStore = phlo.store.get(path)
		const domNonEmpty = (domVal ?? '').trim() !== ''
		const storeEmpty = fromStore === undefined || (typeof fromStore === 'string' && fromStore.trim() === '')
		const domLeads = !isCalc && name !== 'class' && domNonEmpty && storeEmpty
		const S = (v) => v == null ? '' : (typeof v === 'object' ? '' : String(v))
		const apply = (v) => {
			if (name === 'text') el.textContent = S(v)
			else if (name === 'html') app.mod.inner(el, S(v))
			else if (name === 'value') app.mod.value(el, S(v))
			else if (name === 'class'){
				const next = Array.isArray(v) ? v : (v && typeof v === 'object') ? Object.keys(v).filter(k => v[k]) : String(v ?? '').split(/\s+/)
				const uniq = [...new Set(next.filter(Boolean))]
				const prev = meta.attr.cls
				for (let i = 0; i < prev.length; i++) el.classList.remove(prev[i])
				for (let i = 0; i < uniq.length; i++) el.classList.add(uniq[i])
				meta.attr.cls = uniq
			}
			else if (BOOL.has(name)){
				const on = !!v
				app.mod.attr(el, { [name]: on ? '' : null })
				if (name in el) el[name] = on
			}
			else app.mod.attr(el, { [name]: S(v) })
		}
		phlo.store.on(path, apply)
		if (domLeads) phlo.store.set(path, domVal)
		const initial = domLeads ? domVal : fromStore
		apply(initial)
		if (!isCalc && isInputVal) el.oninput = (e) => phlo.store.set(path, e.target.value)
	})
})
object

%template

/phlo/libs/DOM/template.phlo
version 1.0
creator q-ai.nl
description Single Page App client side templating
advice Add cb's to the templates object and output via apply(template: [$name => $rows, $name2 => $rows2, etc])
view

script

line 6
This function executes a template function for each row in 'rows', passing the values of the row as arguments.
app.mod.template = (template, rows) => rows.forEach(row => templates[template](...Object.values(row))),
const templates = {}
object

%timestamps

/phlo/libs/DOM/timestamps.phlo
version 1.0
creator q-ai.nl
description DOM live timestamps
advice Create an app.tsLabels array to overwrite the tsBase labels in any language
view

script

line 6
This code defines a function that calculates the age of elements with a specific data label (dataset.ts) in seconds and converts it into a readable time display, based on a set of time intervals. The function updates this every second and upon startup, providing live updates of elapsed time.
app.tsBase = {seconds: 60, minutes: 60, hours: 24, days: 7, weeks: 4, months: 13, years: 1}
const tsUpdate = () => (ranges = app.tsLabels && (tsValues = Object.values(app.tsBase)) ? Object.fromEntries(app.tsLabels.map((k, i) => [k, tsValues[i]])) : app.tsBase) && objects('[data-ts]').forEach(el => {
	let age = Math.round(Date.now() / 1000) - Number(el.dataset.ts), text = ''
	const future = age < 0
	if (future) age = -age
	for (const [range, multiplier] of Object.entries(ranges)){
		if (text) continue
		if (age / multiplier < 1.6583) text = `${Math.round(age)} ${range}`
		age /= multiplier
	}
	text ||= `${Math.round(age)} ${Object.keys(ranges).at(-1)}`
	text = `${future ? '-' : ''}${text}`
	el.innerText === text || (el.innerText = text)
})
setInterval(() => app.state === 'active' && tsUpdate(), 1000)
setTimeout(tsUpdate, 1)
object

%visible

/phlo/libs/DOM/visible.phlo
version 1.0
creator q-ai.nl
description onVisible and onVisibleIn (for nested scrolling DOM elements) functions
view

script

line 5
This code defines an observation system that responds to element visibility. It registers observations, periodically checks via an update callback, and uses an IntersectionObserver to detect when elements become visible. When observed, the appropriate callback (entering or leaving view) is executed.
phlo.observe = []
phlo.observing = new WeakMap

const onVisible = (els, cbIn, cbOut) => onVisibleIn(els, null, cbIn, cbOut)
const onVisibleIn = (els, root, cbIn, cbOut) => phlo.observe.push({els, root, cbIn, cbOut})

app.updates.push(() => {
	const observers = []
	phlo.observe.forEach(item => objects(item.els).forEach(el => phlo.observing.has(el) || observers.push({el, root: item.root, cbIn: item.cbIn, cbOut: item.cbOut})))
	observers.forEach(item => [phlo.observing.has(item.el) || phlo.observing.set(item.el, 'observe'), (observer = new IntersectionObserver(entries => entries.forEach(entry => entry.isIntersecting ? !item.cbIn && item.cbOut ? [observer.unobserve(entry.target), item.cbOut(entry.target)] : item.cbIn(entry.target) : item.cbIn && item.cbOut && item.cbOut(entry.target)), {root: obj(item.root), threshold: .1})).observe(item.el)])
})

Fields

object

%field_bool

/phlo/libs/Fields/bool.phlo
extends field
class field_bool
version 1.0
creator q-ai.nl
description CMS boolean field
prop

%field_bool -> true

line 7
This node always returns the value '✅', regardless of input or context.
'✅'
prop

%field_bool -> false

line 8
Displays an error message or negative status.
'❌'
method

%field_bool -> label ($record, $CMS)

line 10
Displays a value based on whether the field `$record->{$this->name}` is true (truthy), by returning the 'true' value, otherwise the 'false' value.
$record->{$this->name} ? $this->true : $this->false
method

%field_bool -> input ($record, $CMS)

line 11
Creates a checkbox input with a label and a styled slider. The checkbox is checked if the record's value is not true.
tag('label', inner: input(type: 'checkbox', name: $this->name, value: 1, checked: $record->{$this->name} ? false : null).tag('span', class: 'slider', inner: void))
method

%field_bool -> parse ($record)

line 12
This code converts the value of a field in `$payload` to a boolean and assigns it to the corresponding field in `$record`.
$record->{$this->name} = (bool)%payload->{$this->name};
method

%field_bool -> sql

line 14
Returns an SQL statement that defines a column named from the property with a datatype of tinyint(1) unsigned, suitable for boolean values.
"`$this->name` tinyint(1) unsigned"
method

%field_bool -> nullable

line 15
This node always returns the value false, regardless of the input or context.
false
object

%field_child

/phlo/libs/Fields/child.phlo
extends field
class field_child
version 1.0
creator q-ai.nl
description CMS model child relation field
prop

%field_child -> list

line 7
It counts the number of elements in a list or collection.
'count'
prop

%field_child -> change

line 8
This code always returns false, regardless of the input or situation.
false
prop

%field_child -> create

line 9
This code always returns a negative result, preventing a certain action or creation from being carried out.
false
prop

%field_child -> record

line 10
This is a property of a method that returns a list, probably consisting of child fields or related records.
'list'
method

%field_child -> count ($record, $CMS)

line 12
Creates an HTML link that points to a specific record, with a dynamic URL based on record and object data, and displays the number of related items next to a title.
tag('a', href: slash.$CMS->uriRecord.slash.$record->id.slash.$this->name, class: 'async', inner: $record->getCount($this->name).space.$this->title)
method

%field_child -> last ($record)

line 13
Stores the latest value of a specific field within a record by retrieving the most recent value that matches the field name.
$record->getLast($this->name)
method

%field_child -> label ($record, $CMS)

line 14
This code generates a concatenated list of links for each element in a collection, separated by no separator, and displays a dash if the list is empty or falsy.
implode(loop($record->{$this->name}, fn($child) => $this->link($child, $CMS))) ?: dash
method

%field_child -> input ($record, $CMS)

line 15
Returns the label of the record.
$this->label($record)
method
line 16
Creates a URL based on the record data and adds an HTML link with the generated href, including a CSS class for asynchronous functionality.
$parentPath = $CMS->uriRecord.'/'.$record->{$CMS->model}->id;
$relationshipName = $this->name;
$href = "/{$parentPath}/{$relationshipName}/{$record->id}";
return tag('a', href: $href, class: 'async', inner: $record);
object

%field_date

/phlo/libs/Fields/date.phlo
extends field
class field_date
version 1.0
creator q-ai.nl
description CMS date field
prop

%field_date -> handle

line 7
This node always returns `true`, regardless of the input or context.
true
method

%field_date -> sql

line 9
This indicates that the value is a non-negative date, with the DATE type in SQL and negative values not allowed.
"`$this->name` DATE unsigned"
object

%field_datetime

/phlo/libs/Fields/datetime.phlo
extends field
class field_datetime
version 1.0
creator q-ai.nl
description CMS date-time field
prop

%field_datetime -> handle

line 7
This node always outputs the value true.
true
prop

%field_datetime -> change

line 8
Checks if the name is not equal to 'created', 'changed', or 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])
prop

%field_datetime -> create

line 9
Check if the name is not equal to 'created', 'changed', or 'updated'.
!in_array($this->name, ['created', 'changed', 'updated'])
method

%field_datetime -> label ($record, $CMS)

line 11
Displays a clock icon and a human-readable time if the value exists; otherwise, a dash is shown.
($value = $record->{$this->name}) ? tag('i', class: 'icon clock-'.$this->labelIconClass(time() - $value), inner: void).tag('span', data_ts: $record->{$this->name}, inner: time_human($record->{$this->name})) : dash
method

%field_datetime -> labelIconClass ($value)

line 12
The code block determines the color based on the value: red if the value is greater than 86400, yellow if it is between 3600 and 86400, and blue for lower values.
$value > 86400 ? 'red' : ($value > 3600 ? 'yellow' : 'blue')
method

%field_datetime -> input ($record, $CMS)

line 13
Creates a datetime input field with type 'datetime-local', displays a styled input, with the value set to a formatted date and time value from the record.
input(type: 'datetime-local', name: $this->name, value: date('Y-m-d\TH:i', $record->{$this->name}), class: 'field')
method

%field_datetime -> parse ($record)

line 14
This code processes dates for a record. If the field name is 'created' and has no value yet, the current time is set. For 'changed' or 'updated', the current time is assigned. If a payload value exists, it is converted to a timestamp with strtotime and assigned to the record.
if ($this->name === 'created') $record->created ??= time()
elseif (in_array($this->name, ['changed', 'updated'])) $record->{$this->name} = time()
elseif ($payload = %payload->{$this->name}) $record->{$this->name} = strtotime($payload)
method

%field_datetime -> sql

line 20
The code defines a database column named `$this->name`, of type unsigned integer with a length of 10.
"`$this->name` int(10) unsigned"
object

%field_email

/phlo/libs/Fields/email.phlo
extends field_text
class field_email
version 1.0
creator q-ai.nl
description CMS email field
view

%field_email -> label ($record, $CMS)

line 8
This code generates a mailto link with the email address from the record, allowing users to send an email directly by clicking on the link.
<a href="mailto:{{ $record->{$this->name} }}">{{ $record->{$this->name} }}</a>
object

%field_many

/phlo/libs/Fields/many.phlo
extends field
class field_many
version 1.0
creator q-ai.nl
description CMS model many-to-many relation field
prop

%field_many -> list

line 7
This piece of code returns the text 'label'.
'label'
prop

%field_many -> record

line 8
This node simply passes through the value of 'label', without further processing or context.
'label'
prop

%field_many -> create

line 9
This code always returns true.
false
prop

%field_many -> change

line 10
This node always returns the value `false`, regardless of the input.
false
method

%field_many -> count ($record)

line 12
This code calls the `getCount` method on the `$record` object, with the field name as the parameter, and thus probably returns the number of item(s) or the number of times a specific field appears.
$record->getCount($this->name)
method

%field_many -> label ($record, $CMS)

line 13
Loop through the elements in the relationship, create a link for each, and return the result; if empty, a hyphen is displayed.
loop($record->{$this->name}, fn($relation) => $this->link($relation), lf) ?: dash
method
line 14
Create a hyperlink with a dynamic URL based on record data, including a class for asynchronous action, and display the record as the link text.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $record)
method

%field_many -> input ($record, $CMS)

line 15
Displays a label based on the record and CMS.
$this->label($record, $CMS)
method

%field_many -> sql

line 17
The result is an empty array, indicating that this method or function does not return data or perform any operations.
[]
object

%field_number

/phlo/libs/Fields/number.phlo
extends field
class field_number
version 1.0
creator q-ai.nl
description CMS number field
prop

%field_number -> decimals

line 7
Returns the number with zero decimals, or an integer value.
prop

%field_number -> length

line 8
This code simply returns the number 5, which probably indicates the length of a field or a similar value.
5
prop

%field_number -> min

line 9
This code always returns 0.
method

%field_number -> label ($record, $CMS)

line 11
Formats a number from the record with a specified number of decimals, using a comma as the decimal separator and a period as the thousand separator.
number_format($record->{$this->name}, $this->decimals, comma, dot)
method

%field_number -> input ($record, $CMS)

line 12
Creates a numeric input field with dynamic value, default value, step size based on decimals, minimum value, and a CSS class.
input(type: 'number', name: $this->name, value: $record->{$this->name} ?? $this->default, step: $this->decimals ? dot.str_repeat('0', $this->decimals - 1).'1' : null, min: $this->min, class: 'field')
method

%field_number -> sql

line 14
This code generates an SQL field definition that defines a numeric field, depending on the presence of decimals. If there are decimals, a 'decimal' data type is used with a custom precision and scale; otherwise, 'smallint' is used. In both cases, the unsigned option is added.
"`$this->name` ".($this->decimals ? 'decimal('.($this->length + $this->decimals).comma.$this->decimals.')' : "smallint($this->length)").' unsigned'
object

%field_parent

/phlo/libs/Fields/parent.phlo
extends field
class field_parent
version 1.0
creator q-ai.nl
description CMS model parent relation field
method

%field_parent -> label ($record, $CMS)

line 7
Checks whether a field is a model object; if so, it generates a link, otherwise a dash is displayed.
is_a($obj = $record->{$this->name}, 'model') ? $this->link($obj) : dash
method

%field_parent -> input ($record, $CMS)

line 8
This code generates an HTML select element with options based on a list of options. For each option, it checks if it matches a value from the record, and if so, the 'selected' attribute is added.
select(name: $this->name, inner: loop($this->options, fn($parent) => '<option'.($parent->id === $record->{$this->name}?->id ? ' selected' : void).' value="'.$parent->id.'">'.$parent, void))
method
line 9
Create an HTML anchor link with a dynamically generated URL and add the content or use the record as content, with a CSS class 'async'.
tag('a', href: slash.($record::$uriRecord ?? $record::class).slash.$record->id, class: 'async', inner: $content ?? $record)
prop

%field_parent -> options

line 10
Retrieves all available records within the context of the class or object.
$this->obj::records()
method

%field_parent -> sql

line 12
Defines a varchar field named `$this->name` that can contain up to 10 characters.
"`$this->name` varchar(10)"
object

%field_password

/phlo/libs/Fields/password.phlo
extends field
class field_password
version 1.0
creator q-ai.nl
description CMS password field
prop

%field_password -> list

line 7
This node always returns the value false, regardless of any input or context.
false
prop

%field_password -> required

line 8
This node always returns the value 'true', which means that the field is always marked as required.
true
prop

%field_password -> minlength

line 9
A minimum length of 8 characters for a password.
8
prop

%field_password -> placeholder

line 10
It returns a text label indicating that it is the field for the new password.
'Nieuw wachtwoord'
method

%field_password -> input ($record, $CMS)

line 12
Creates an input field with type, name, placeholder, and a fixed CSS class.
input(type: $this->type, name: $this->name, placeholder: $this->placeholder, class: 'field')
method

%field_password -> label ($record, $CMS)

line 13
Displays a fixed, encrypted, or hidden password label, regardless of input or context.
'••••••••'
method

%field_password -> parse ($record)

line 14
This code retrieves the password value from the payload, removes any spaces, and if a password is provided, hashes it with bcrypt and stores it in the record.
($password = trim(%payload->{$this->name})) && $record->{$this->name} = password_hash($password, PASSWORD_BCRYPT)
method

%field_password -> sql

line 16
Indicates that the 'name' field is defined as a string with a maximum length of 60 characters in an SQL table.
"`$this->name` char(60)"
object

%field_price

/phlo/libs/Fields/price.phlo
class field_price
version 1.0
creator q-ai.nl
description CMS price field
extends field_number
prop

%field_price -> decimals

line 7
This code returns the number 2, which probably indicates that the number should be displayed or used with 2 decimal places.
2
object

%field_select

/phlo/libs/Fields/select.phlo
extends field
class field_select
version 1.0
creator q-ai.nl
description CMS select field
method

%field_select -> input ($record, $CMS)

line 7
Generates a `<select>` element with a dynamic name and options, marking the option as selected based on the record.
select(name: $this->name, inner: loop($this->options, fn($option) => "<option".($record->{$this->name} === $option ? ' selected' : void).">$option", void))
method

%field_select -> sql

line 9
This defines an SQL column named `$this->name` that is of ENUM type, including options concatenated into a list like `'option1','option2',...`.
"`$this->name` enum('".implode("','", $this->options)."')"
object

%field_text

/phlo/libs/Fields/text.phlo
extends field
class field_text
version 1.0
creator q-ai.nl
description CMS text field
prop

%field_text -> length

line 7
This node returns the value 100, probably the length of a text or a similar numerical value.
100
prop

%field_text -> multiline

line 8
This property checks whether the length of a text is more than 250 characters.
$this->length > 250
method

%field_text -> input ($record, $CMS)

line 10
Selects whether to use a multiple input field or a single input field based on a condition.
$this->multiline ? $this->inputMulti($record) : $this->inputField($record)
method

%field_text -> inputField ($record)

line 11
Create an HTML input field with the type, name, value, maximum length, placeholder, and CSS class based on the object properties. The value is escaped if it exists; otherwise, a default value is used.
input(type: $this->type, name: $this->name, value: ($value = $record->{$this->name}) ? esc($value) : $this->default, maxlength: $this->length, placeholder: $this->placeholder, class: 'field')
method

%field_text -> inputMulti ($record)

line 12
Creates a textarea element with name, placeholder, and class. If a value exists in the record for this name, it is escaped and filled in; otherwise, a default value is used if available.
textarea(name: $this->name, inner: ($value = $record->{$this->name}) ? esc($value) : $this->default ?? void, placeholder: $this->placeholder, class: 'field')
method

%field_text -> sql

line 14
Defines an SQL field named `$this->name`, data type varchar, and length `$this->length`.
"`$this->name` varchar($this->length)"
object

%field_token

/phlo/libs/Fields/token.phlo
extends field
class field_token
version 1.0
creator q-ai.nl
description CMS token type field
prop

%field_token -> length

line 7
Returns the length of the content, in this case 8.
8
prop

%field_token -> default

line 8
Pick up a token with a length equal to the value of `$this->length`.
token($this->length)
prop

%field_token -> create

line 9
This code always returns false, regardless of the input or circumstances.
false
prop

%field_token -> change

line 10
This code always returns `false`, regardless of the context or input.
false
prop

%field_token -> search

line 11
This code ensures that a position is always returned, regardless of the input. It acts as a confirmation that the search was successful or that the result is always true.
true
method

%field_token -> label ($record, $CMS)

line 13
Creates an HTML-div element with the escaped value of a field from the record as its content.
tag('div', inner: esc($record->{$this->name}))
method

%field_token -> parse ($record)

line 15
Sets the value of a field to the value in the record if it does not already exist, or changes it otherwise to a default value.
$record->{$this->name} ??= $this->default
method

%field_token -> sql

line 17
This code defines a varchar column with a name determined by the value of `$this->name` and a length determined by `$this->length`.
"`$this->name` char($this->length)"
object

%field_virtual

/phlo/libs/Fields/virtual.phlo
extends field
class field_virtual
version 1.0
creator q-ai.nl
description CMS virtual field
prop

%field_virtual -> create

line 7
This method or function always returns false, probably intended to prevent or mark a certain operation or validation as unsuccessful.
false
prop

%field_virtual -> change

line 8
This code will always return false.
false
method

%field_virtual -> sql

line 10
This node returns an empty array without further processing or data.
[]

Files

object

%CSV

/phlo/libs/Files/CSV.phlo
version 1.0
creator q-ai.nl
description CSV reader library
static

CSV :: __handle

line 5
This appears to be a property that returns a file or parent reference with a dynamic path constructed from variables for the path and filename.
"CSV/$path$filename"
method

%CSV -> __construct (string $filename, ?string $path = null)

line 6
Check if the path is set, and if not, use a default path; assemble the filename and extension; check if the CSV file is readable and if so, call the read function.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.csv'
if (is_readable($this->objFile)) $this->objRead()
readonly

%CSV -> objFile:string

line 12
This node reads a CSV file and links it to a variable so that the data can be processed further.
method

%CSV -> objRead

line 14
Process a CSV file by opening it, reading the first row (headers), determining the correct delimiter based on the presence of commas or semicolons, splitting the headers, and then reading all rows. Each row is associated with the headers and stored in an array.
$fp = fopen($this->objFile, 'r+')
$headers = str_replace([dq, cr, lf], void, fgets($fp))
$delimiter = substr_count($headers, comma) > substr_count($headers, semi) ? comma : semi
$headers =  explode($delimiter, $headers)
while ($row = fgetcsv($fp, null, $delimiter)) $this->objData[] = array_combine($headers, $row)
fclose($fp)
object

%file

/phlo/libs/Files/file.phlo
version 1.0
creator q-ai.nl
description File library
static

file :: __handle

line 5
This code constructs a path by combining 'file/' with a variable '$file' and, if present, an optional name '$name' that is added with a slash.
"file/$file".($name ? "/$name" : void)
method

%file -> __construct (public string $file, ?string $name = null, $contents = null, ...$args)

line 6
Sets a name if provided, writes content to the file if content is a string, and imports additional objects or arguments if they are present.
$name && $this->name = $name
is_string($contents) && $this->write($contents)
$args && $this->objImport(...$args)
method

%file -> append (string $data)

line 12
Add the specified text to the end of the file without overwriting it.
file_put_contents($this->file, $data, FILE_APPEND)
prop

%file -> basename

line 13
Retrieves the base name (filename without path) of the specified file.
pathinfo($this->file, PATHINFO_BASENAME)
method

%file -> base64

line 14
Converts the content of the file to a Base64-encoded string.
base64_encode($this->contents)
method

%file -> contents

line 15
Read the contents of the file specified by the filename in a property.
file_get_contents($this->file)
method

%file -> contentsINI (bool $parse = true)

line 16
Creates an INI string, parses the content depending on the parameter, and returns the result with type transparency if `$parse` is true.
parse_ini_string($this->contents, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW)
method

%file -> contentsJSON ($assoc = null)

line 17
Converts the content of a JSON string into a PHP variable, returning the result as an array or object depending on the parameter.
json_decode($this->contents, $assoc)
method

%file -> copy ($to)

line 18
Creates a copy of the file and saves it at the specified location.
copy($this->file, $to)
method

%file -> created

line 19
Returns the creation time of the file.
filectime($this->file)
method

%file -> createdAge

line 20
This node calculates the age since creation by comparing the creation date with the current date.
age($this->created)
method

%file -> createdHuman

line 21
This code converts a timestamp into a human-readable format.
time_human($this->created)
method

%file -> curl ($type = null, $filename = null)

line 22
Creates a new CURLFile object with the specified filename, type, and optionally a different filename for the upload.
new CURLFile($this->file, $type, $filename)
method

%file -> delete

line 23
Performs a check to see if the file exists and attempts to delete it. Then a debug message is logged indicating whether the deletion was successful or not.
first($deleted = $this->exists && unlink($this->file), debug($deleted ? "Deleted $this->basename" : "Could not delete $this->basename"))
method

%file -> exists

line 24
Checks whether the file stored in the variable `$this->file` exists on the system.
file_exists($this->file)
prop

%file -> ext

line 25
Extracts the file extension from the filename.
pathinfo($this->name, PATHINFO_EXTENSION)
prop

%file -> filename

line 26
Extracts the filename without extension from the path or filename stored in the variable.
pathinfo($this->file, PATHINFO_FILENAME)
method

%file -> getLine

line 27
Takes a line from the file, removes any whitespace at the end, and returns it; if the end of the file is reached, returns false.
($line = fgets($this->pointer)) === false ? false : rtrim($line)
method

%file -> getLength (int $length)

line 28
Reads a certain number of bytes ($length) from the file via the specified pointer.
fread($this->pointer, $length)
method

%file -> is (string $file)

line 29
Compares whether the specified filename is equal to the internal property `$file`.
$file === $this->file
method

%file -> md5

line 30
This code calculates the MD5 hash of the file displayed by the `$file` property.
md5_file($this->file)
prop

%file -> mime

line 31
Determines the MIME type of a file based on the filename.
mime($this->name)
method

%file -> modified

line 32
Returns the last modification date and time of the file.
filemtime($this->file)
method

%file -> modifiedAge

line 33
Returns the age based on the modified date.
age($this->modified)
method

%file -> modifiedHuman

line 34
This node returns the most recent change in a human-readable time format.
time_human($this->modified)
method

%file -> move ($to)

line 35
Move the file to a new location and update the internal reference to the changed filename or path.
rename($this->file, $to) && $this->file = $to
prop

%file -> name

line 36
This retrieves the filename without the path, usually used to get the name without the directory.
$this->basename
method

%file -> output ($download = false)

line 37
Creates an output with the content, name, and optionally downloads a file.
output($this->contents, $this->name, $download)
prop

%file -> path

line 38
This code returns the absolute path to the directory containing the file, followed by a slash.
realpath(pathinfo($this->file, PATHINFO_DIRNAME)).slash
prop

%file -> pathRel

line 39
Checks whether the file path starts with a specific relative root path, and if so, returns the path from that point; otherwise, the full path is displayed.
str_starts_with($this->file, $relRoot = dirname(dirname($_SERVER['DOCUMENT_ROOT'])).slash) ? substr($this->file, strlen($relRoot)) : $this->file
prop

%file -> pointer

line 40
This code opens a file in read and write modes.
fopen($this->file, 'r+')
method

%file -> readable

line 41
Checks whether the file linked to the variable 'file' is readable.
is_readable($this->file)
method

%file -> src

line 42
Creates a data-URI with the MIME type and the base64 data, suitable for use in image or file references.
"data:$this->mime;base64,$this->base64"
method

%file -> size

line 43
Returns the size of the file.
filesize($this->file)
method

%file -> sizeHuman (int $precision = 0)

line 44
Converts the size of a file to a human-readable string with optional precision.
size_human($this->size, $precision)
method

%file -> sha1

line 45
This method calculates the SHA-1 hash of the file.
sha1_file($this->file)
method

%file -> shortenTo (int $length)

line 46
This method shortens a filename to a specified length. If the name is already shorter than the length, the original name is returned. Otherwise, an ellipsis is added so that the final length does not exceed the specified value, including the file extension.
strlen($this->name) <= $length ? $this->name : substr($this->name, 0, $length - strlen($this->ext) - 3).dot.dot.dot.$this->ext
method

%file -> title

line 47
Convert the filename without extension into a title by capitalizing the first letter and replacing underscores or other non-letters with spaces.
ucfirst(strtr(pathinfo($this->name, PATHINFO_FILENAME), [us => space]))
method

%file -> token ($length = 20)

line 48
Generates a token with a specified length, using the SHA-1 hash function by default to create the token.
token($length, sha1: $this->sha1)
method

%file -> type

line 49
Extracts the part of the MIME value before the slash, for example the type such as 'image' or 'video'.
substr($this->mime, 0, strpos($this->mime, slash))
method

%file -> touch

line 50
Creates the file with the specified name (provided by `$this->file`) if it does not already exist or updates the file's access time.
touch($this->file)
method

%file -> writeable

line 51
checks if the file is writable.
is_writeable($this->file)
method

%file -> writeINI ($data, bool $deleteEmpty = false)

line 52
Writes data to an INI file, where empty sections are not removed unless specified, and processes the data by escaping each value and formatting as key-value pairs with double quotes, separated by new lines.
$this->write(!$deleteEmpty || $data ? loop($data, fn($value, $key) => $key.' = '.dq.strtr($value, [dq => bs.dq, lf => '\n']).dq, lf).lf : void, $deleteEmpty)
method

%file -> writeJSON ($data, bool $deleteEmpty = false)

line 53
This code calls a method to write data. If `$deleteEmpty` is not true or `$data` is present, the data is JSON-encoded and passed; otherwise, no data is written.
$this->write(!$deleteEmpty || $data ? json_encode($data, jsonFlags) : void, $deleteEmpty)
method

%file -> writeJSONplain ($data, bool $deleteEmpty = false)

line 54
This code checks whether empty data should be written. If $deleteEmpty is false or $data is not empty, the data is evaluated as JSON code and passed to a write function; otherwise, nothing is written.
$this->write(!$deleteEmpty || $data ? json_encode($data) : void, $deleteEmpty)
method

%file -> write (string $data, bool $deleteEmpty = false)

line 55
This method writes data to a file and deletes the file if the data is empty and the parameter is set to true. On successful write, a debug message is logged; otherwise, an error is reported.
if (!$data && $deleteEmpty) return $this->delete
if ($written = file_put_contents($this->file, $data) !== false) debug('Written '.$this->basename.' ('.$this->sizeHuman.')')
else error('Could not write '.$this->file)
return $written
method

%file -> objInfo

line 62
This code creates an array where keys are combined and values are derived from the current object properties. It adds basic information and, depending on a condition, also includes additional file information such as size, creation and modification dates, and MIME type.
array_combine($keys = array_merge(['file', 'name', 'exists'], $this->exists ? ['sizeHuman', 'createdHuman', 'modifiedHuman', 'mime'] : []), loop($keys, fn($arg) => $this->$arg))
object

%img

/phlo/libs/Files/img.phlo
version 1.0
creator q-ai.nl
description GD/img library
static

img :: detect ($data)

line 5
This code detects the image format based on the first bytes of the data and returns the corresponding file extension ('jpg', 'png', 'gif', 'webp', 'bmp', 'tiff') by comparing specific headers.
$header = substr($data, 0, 12)
if (substr($header, 0, 3) === "\xFF\xD8\xFF") return 'jpg'
if (substr($header, 0, 8) === "\x89PNG\x0D\x0A\x1A\x0A") return 'png'
if (substr($header, 0, 6) === 'GIF87a' || substr($header, 0, 6) === 'GIF89a') return 'gif'
if (substr($header, 0, 4) === 'RIFF' && substr($header, 8, 4) === 'WEBP') return 'webp'
if (substr($header, 0, 2) === "BM") return 'bmp'
if (substr($header, 0, 4) === "\x49\x49\x2A\x00" || substr($header, 0, 4) === "\x4D\x4D\x00\x2A") return 'tiff'
static

img :: search ($search)

line 15
Searches for image results on Google based on a formatted search term, retrieves multiple base64-encoded JPEG data from the HTML, sorts them by size, selects a few, randomly chooses one, and finally decodes it into an image file.
$q = strtr($search, [dash => '+'])
$DOM = HTTP("https://www.google.com/search?q=$q&source=lnms&tbm=isch&tbs=", agent: true)
$sources = regex_all('/var s=\'data:image\/jpeg;base64,([^\']{1000,})/', $DOM)[1]
usort($sources, fn($a, $b) => strlen($b) <=> strlen($a))
$sources = array_slice($sources, 0, 5)
shuffle($sources)
$source = current($sources)
$source = base64_decode($source)
return $source
static

img :: __handle

line 27
Outputs the value of the variable `$file` before the string "img/".
"img/$file"
method

%img -> __construct (public string $file)

line 28
The constructor accepts a string parameter that is stored in a public property, probably intended to represent a filename or path for the node.
prop

%img -> src:GdImage

line 30
Load an image from a file by reading its contents and then converting it into an image resource.
imagecreatefromstring(file_get_contents($this->file))
prop

%img -> width

line 31
This node displays the width of the image by retrieving the width of the image using the `imagesx` function on the source image.
imagesx($this->src)
prop

%img -> height

line 32
This node returns the width of an image by retrieving the width of the image with `imagesx()` on the source (`$this->src`).
imagesx($this->src)
method

%img -> scale ($width, $height = false, $crop = true)

line 34
Performs a scale operation based on specified width and height, including optional cropping and aspect ratio preservation, with correct image resampling and transparency support.
if (!$width && !$height) return $this
$scale = 1
if ($width && $height) $scale = $crop ? max($width / $this->width, $height / $this->height) : min($width / $this->width, $height / $this->height)
elseif ($width) $scale = $width / $this->width
elseif ($height) $scale = $height / $this->height
if ($scale > 1) return $this
if ($width && $height && $this->width / $this->height != $width / $height){
	if ($width / $height > $this->width / $this->height){
		if ($crop){
			$destX = $width
			$destY = round($width / $this->width * $this->height)
			if ($crop === 'top') $offsetY = 0
			elseif ($crop === 'bottom') $offsetY = -$destY - -$height
			else $offsetY = -round(($destY - $height) / 2)
		}
		else {
			$destX = round($this->width * $height / $this->height)
			$destY = $height
			$width = $destX
			$offsetY = 0
		}
		$offsetX = 0
	}
	else {
		if ($crop){
			$destX = round($height / $this->height * $this->width)
			$destY = $height
			$offsetX = -round (($destX - $width) / 2)
		}
		else {
			$destX = $width
			$destY = round($this->height * $width / $this->width)
			$height = $destY
			$offsetX = 0
		}
		$offsetY = 0
	}
	$destImg = imagecreatetruecolor($width, $height)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, $offsetX, $offsetY, 0, 0, $destX, $destY, $this->width, $this->height)
}
else {
	if ($width){
		$destX = $width
		$destY = $width / $this->width * $this->height
	}
	elseif ($height){
		$destX = $height / $this->height * $this->width
		$destY = $height
	}
	$destImg = imagecreatetruecolor($destX, $destY)
	imagealphablending($destImg, false)
	imagesavealpha($destImg, true)
	imagecopyresampled($destImg, $this->src, 0, 0, 0, 0, $destX, $destY, $this->width, $this->height)
}
$this->src = $destImg
return $this
method

%img -> source

line 95
Captures the output of the `imagejpeg()` function into a buffer and returns it as a string, making the image available in memory for further processing.
ob_start()
imagejpeg($this->src)
return ob_get_clean()
method

%img -> save ($file = null)

line 101
Saves the image file, depending on the extension; for 'png' and 'webp' compression is applied, for 'gif' it is saved without compression, and by default JPEG is used with a quality of 85%.
$file && $this->file = $file
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION))
if ($ext === 'png') return imagepng($this->src, $this->file, 8)
if ($ext === 'gif') return imagegif($this->src, $this->file)
if ($ext === 'webp'){
	imageistruecolor($this->src) || imagepalettetotruecolor($this->src)
	return imagewebp($this->src, $this->file)
}
return imagejpeg($this->src, $this->file, 85)
object

%INI

/phlo/libs/Files/INI.phlo
version 1.0
creator q-ai.nl
description Generic INI library
prop

%INI -> objFile:string

line 5
This node possibly initializes a file or object, depending on the context, and performs a setup or initialization for the selected file or object.
static

INI :: __handle

line 7
This code constructs a filename by combining a path and filename, and adds '/0' if the parameter `$parse` is not true.
"INI/$path$filename".(!$parse ? '/0' : void)
method

%INI -> __construct (string $filename, ?string $path = null, bool $parse = true)

line 8
This constructor initializes a filename based on an optional path and the name, by applying dot replacement. It then checks if the ini file is readable and reads it if possible, depending on the parse parameter.
$path ??= data
$this->objFile = $path.strtr($filename, [slash => dot]).'.ini'
if (is_readable($this->objFile)) $this->objRead($parse)
method

%INI -> objRead ($parse = true)

line 14
This code reads an INI file with parse_ini_file, storing the contents in objData. Then, the last value of that array is evaluated, and the status of objChanged is set to false. The result of the last function is returned.
last($this->objData = parse_ini_file($this->objFile, true, $parse ? INI_SCANNER_TYPED : INI_SCANNER_RAW), $this->objChanged = false, $this)
method

%INI -> objWrite

line 15
Writes the normalized data to a file by formatting each key-value pair, properly escaping, and then writing everything together with new lines.
file_put_contents($this->objFile, loop($this->objData, fn($value, $key) => $key.' = '.dq.strtr($value, [dq => bs.dq, lf => '\n']).dq, lf).lf)
method

%INI -> __destruct

line 17
Executes the write method when changes have occurred.
$this->objChanged && $this->objWrite()
object

%JSON

/phlo/libs/Files/JSON.phlo
version 1.0
creator q-ai.nl
description Generic JSON library
static

JSON :: __handle

line 5
Concatenates a path and filename with a condition that, if `$assoc` is a boolean value, a slash and the integer version of `$assoc` are added; otherwise, nothing happens.
"JSON/$path$filename".(is_bool($assoc) ? slash.(int)$assoc : void)
method

%JSON -> __construct (string $filename, ?string $path = null, $assoc = null)

line 6
Checks if the $path variable is null and sets it to 'data' if necessary. Builds the full path to the JSON file. If the file is readable, a read function is called with the $assoc parameter.
$path ??= data
$this->objFile = "$path$filename.json"
if (is_readable($this->objFile)) $this->objRead($assoc)
readonly

%JSON -> objFile:string

line 12
This code fetches a JSON file and converts it into an object, which is then stored in a variable or passed on for further processing.
method

%JSON -> objTouch

line 14
This node indicates that changes have been made, probably to later check if updates are needed or actions need to be taken.
$this->objChanged = true
method

%JSON -> objRead ($assoc = null)

line 15
Reads JSON data from a file, stores it in a class property, and returns the class itself for further method chaining.
last($data = json_read($this->objFile, $assoc), $this->objData = $assoc || is_array($data) ? $data : get_object_vars($data), $this->objChanged = false, $this)
method

%JSON -> objWrite ($data, $flags = null)

line 16
Performs a JSON display to a file and updates the object's status based on the result of the write operation.
first($written = json_write($this->objFile, $data, $flags), $written && $this->objChanged = false)
method

%JSON -> __destruct

line 18
Checks for any changes and then saves the data if there are any.
$this->objChanged && $this->objWrite($this->objData)
object

%PDF

/phlo/libs/Files/PDF.phlo
version 1.0
creator q-ai.nl
description PDF generator en reader
static

PDF :: toText (string $file):string

line 5
Executes the 'pdftotext' command on a PDF file via a subprocess, reads the output and error messages, checks for errors, and returns the text without leftover form feed characters.
$process = proc_open('pdftotext '.escapeshellarg($file).' -', [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes)
if (!is_resource($process)) return null
fclose($pipes[0])
$text = stream_get_contents($pipes[1])
fclose($pipes[1])
$error = stream_get_contents($pipes[2])
fclose($pipes[2])
($code = proc_close($process)) && error("PDFToText Error: pdftotext command failed with code $code. Error: $error")
return rtrim($text, "\f")
prop

%PDF -> title

line 17
This node does not pass any value, which possibly means that there is no title or that the title is not set.
null
prop

%PDF -> author

line 18
This code always returns null, regardless of the input.
null
prop

%PDF -> subject

line 19
This node always returns null, regardless of the input or other conditions.
null
prop

%PDF -> keywords

line 20
This node does not return any data and therefore returns a null value.
null
prop

%PDF -> creator

line 21
This node returns a string consisting of the text 'Phlo ' followed by the content of the variable or property 'phlo', and then the URL '(https://phlo.tech/)'.
'Phlo '.phlo.' (https://phlo.tech/)'
prop

%PDF -> filename

line 23
Returns a file named "Download.pdf" for download.
'Download.pdf'
prop

%PDF -> mode

line 24
This node returns the value 'D'.
'D'
method

%PDF -> fromHTML ($HTML)

line 26
Creates a new mpdf instance, sets metadata based on existing properties, saves the HTML content, and returns the generated PDF file with the specified filename and output mode.
$mpdf = new \Mpdf\Mpdf
$this->title && $mpdf->SetTitle($this->title)
$this->author && $mpdf->SetAuthor($this->author)
$this->subject && $mpdf->SetSubject($this->subject)
$this->keywords && $mpdf->SetKeywords($this->keywords)
$this->creator && $mpdf->SetCreator($this->creator)
$mpdf->WriteHTML($HTML)
return $mpdf->Output($this->filename, $this->mode)
object

%XLSX

/phlo/libs/Files/XLSX.phlo
version 1.0
creator q-ai.nl
description XLSX reader library
method

%XLSX -> __construct (string $file)

line 5
Loads an XLSX file by unpacking the ZIP structure, reads cell data and shared strings, and converts the data into an array structure with headers. Processing of worksheets, rows, and cells is done with regex and string manipulation, ensuring shared strings are correctly evaluated and cell references are converted into indices.
$sheets = []
$shared = []
$sheetNames = []
$zip = new ZipArchive()
if ($zip->open($file) !== true) error('Error opening XLSX: '.esc($file))
for ($i = 0; $i < $zip->numFiles; $i++){
	$name = $zip->getNameIndex($i)
	if ($name === false) continue
	if (dirname($name) === 'xl/worksheets') $sheets[filter_var($name, FILTER_SANITIZE_NUMBER_INT)] = $zip->getFromIndex($i)
	elseif ($name === 'xl/sharedStrings.xml'){
		$xml = $zip->getFromIndex($i)
		if (!preg_match_all('/<t[^>]*>(.*?)<\/t>/s', $xml, $m)) error('Error reading shared lib in XLSX')
		$shared = array_map(fn($t) => html_entity_decode($t, ENT_QUOTES | ENT_XML1, 'UTF-8'), $m[1])
	}
	elseif ($name === 'xl/workbook.xml'){
		$xml = $zip->getFromIndex($i)
		if (!preg_match_all('/<sheet[^>]*name="([^"]+)"[^>]*sheetId="([0-9]+)"/', $xml, $m)) error('Error reading XLSX workbook')
		$sheetNames = $m[1]
	}
}
$zip->close()
$toIndex = fn($letters) => array_reduce(str_split(strtoupper($letters)), fn($n, $c) => $n * 26 + ord($c) - 64, 0) - 1
$isShared = fn($attrs) => preg_match('/\bt="s"\b/', $attrs) === 1
foreach ($sheets AS $sheetID => $sheet){
	$name = $sheetNames[$sheetID - 1] ?? 'Sheet '.$sheetID
	if (!preg_match('/<row[^>]*>(.+)<\/row>/s', $sheet, $m)) error('Error parsing XLSX sheet')
	$rowsXml = preg_split('/<\/row><row[^>]*>/', $m[1]) ?: []
	$headerMap = []
	$isHeader = true
	foreach ($rowsXml AS $rowXml){
		$rowXml = preg_replace('/<c([^>]*)\/>/', '<c$1></c>', $rowXml)
		if (!preg_match_all('/<c r="([A-Z]+)[0-9]+"([^>]*)>(?:<f\b[^>]*\/?>)?(?:(?:<v>([^<]*)<\/v>)|(?:<is>.*?<t[^>]*>(.*?)<\/t>.*?<\/is>))?<\/c>/s', $rowXml, $mm)) error('Error parsing XLSX row')
		if ($isHeader){
			foreach (array_keys($mm[0]) AS $i){
				$col = $toIndex($mm[1][$i])
				$attrs = $mm[2][$i]
				$valV = $mm[3][$i] ?? null
				$valIS = $mm[4][$i] ?? null
				$val = $valV !== null && $valV !== '' ? $valV : ($valIS !== null && $valIS !== '' ? html_entity_decode($valIS, ENT_QUOTES | ENT_XML1, 'UTF-8') : null)
				$txt = $isShared($attrs) ? ($shared[$val] ?? null) : $val
				$headerMap[$col] = $txt !== null && $txt !== '' ? $txt : 'col'.$col
			}
			$isHeader = false
		}
		else {
			$rowArr = []
			foreach (array_keys($mm[0]) AS $i){
				$col = $toIndex($mm[1][$i])
				$attrs = $mm[2][$i]
				$valV = $mm[3][$i] ?? null
				$valIS = $mm[4][$i] ?? null
				$val = $valV !== null && $valV !== '' ? $valV : ($valIS !== null && $valIS !== '' ? html_entity_decode($valIS, ENT_QUOTES | ENT_XML1, 'UTF-8') : null)
				$key = $headerMap[$col] ?? 'col'.$col
				$rowArr[$key] = $isShared($attrs) ? ($shared[$val] ?? null) : $val
			}
			$this->objData[$name][] = $rowArr
		}
	}
}

Lib Functions

function

button(...$args):string

/phlo/libs/form.tags.phlo line 5
Creates a 'button' element with the given arguments as attributes or content.
tag('button', ...$args)
function

decrypt($encrypted, $key):string

/phlo/libs/encryption.phlo line 6
Decode the base64-encoded input and check if the decoding is successful and the length is sufficient. If so, the nonce is extracted from the first bytes and the rest is used as ciphertext. Then, the secret data is decrypted using a hash of the provided key and the sodium_crypto_secretbox_open function. If any of the checks fail, false is returned.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
function

en($text, ...$args)

/phlo/libs/lang.phlo line 9
This node translates the input text into English, using the same translation function with the provided text and arguments.
%lang->translation('en', $text, ...$args)
function

encrypt($data, $key):string

/phlo/libs/encryption.phlo line 5
This code generates a random nonce, encrypts the data using a secret key function, and then encodes the result in Base64 for secure transmission or storage.
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

field($type, ...$args)

/phlo/libs/field.phlo line 6
This defines a function that creates a new field, where the function name is dynamically constructed based on the type. It uses the `$type` argument and additional arguments to generate a field of that specific type.
phlo("field_$type", ...$args, type: $type)
function

input(...$args):string

/phlo/libs/form.tags.phlo line 6
This node generates an HTML `<input>` element with the specified arguments as attributes or content.
tag('input', ...$args)
function

n8n($webhook, ?array $data = null, $test = false)

/phlo/libs/n8n.phlo line 5
Makes a POST request to a webhook URL, based on server data and test mode, with optional data included.
HTTP(%creds->n8n->server.'webhook'.($test ? '-test' : '').'/'.$webhook, POST: $data)
function

n8n_test($webhook, ?array $data = null)

/phlo/libs/n8n.phlo line 6
Call a function named 'n8n' with the webhook, an optional data array, and a boolean parameter (probably for a special mode or debug).
n8n($webhook, $data, true)
function

nl($text, ...$args)

/phlo/libs/lang.phlo line 8
Translate the provided text into Dutch, possibly with additional arguments for interpolation or context.
%lang->translation('nl', $text, ...$args)
function

select(...$args):string

/phlo/libs/form.tags.phlo line 7
Create a 'select' element with the given arguments.
tag('select', ...$args)
function

textarea(...$args):string

/phlo/libs/form.tags.phlo line 8
Creates an HTML `<textarea>` element with the given arguments.
tag('textarea', ...$args)

Phlo Functions

function

active(bool $cond, string $classList = void):string

/phlo/phlo.php line 265
Executes a condition where, if `$cond` is true or `$classList` is not empty, a class attribute is added with the value of `$classList` and possibly 'active' afterwards, separated by a space if `$classList` already exists and `$cond` is true.
return $cond || $classList ? ' class="'.$classList.($cond ? ($classList ? space : void).'active' : void).'"' : void;
function

age(int $time):int

/phlo/phlo.php line 266
Returns the number of seconds since the specified time.
return time() - $time;
function

age_human(int $age):string

/phlo/phlo.php line 267
Converts an age in seconds to a human-understandable age display using another function.
time_human(time() - $age);
function

apcu($key, $cb, int $duration = 3600, bool $log = true):mixed

/phlo/phlo.php line 268
This code attempts to retrieve a value from the APCu cache or generate it via a callback. The value is then returned, and if logging is enabled, a debug message is sent with details about the cache key and the contents or type of the value.
return first($value = apcu_entry($key, $cb, $duration), $log && debug('C: '.(strlen($key) > 58 ? substr($key, 0, 55).'...' : $key).(is_array($value) ? ' ('.count($value).')' : (is_numeric($value) ? ":$value" : (is_string($value) ? ':string:'.strlen($value) : colon.gettype($value))))));
function

apply(...$cmds):never

/phlo/phlo.php line 217
Execute a JSON-encoded response with the results of `$cmds`, after checking the CLI mode, or when headers have already been sent, or in streaming context. Apply debug to `$cmds` if enabled, and then stop processing.
cli || headers_sent() || phlo('app')->streaming || [header('Content-Type: application/json'), header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'), header('Pragma: no-cache'), header('X-Content-Type-Options: nosniff')];
debug && $cmds = debug_apply($cmds);
die(json_encode($cmds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
function

arr(...$array):array

/phlo/phlo.php line 269
Does not remove elements and returns the received array directly.
return $array;
function

auth_log(string $user):int|false

/phlo/phlo.php line 270
Write a line to the log file with the current date and time, the username, and the visitor's IP address, and append it to the end of the file.
return file_put_contents(data.'access.log', date('j-n-Y H:i:s')." - $user - $_SERVER[REMOTE_ADDR]\n", FILE_APPEND);
function

camel(string $text):string

/phlo/phlo.php line 271
This function converts a text to camelCase by removing spaces and starting words with a capital letter, then making the first letter of the result lowercase.
return lcfirst(str_replace(space, void, ucwords(lcfirst($text))));
function

chunk(...$cmds):void

/phlo/phlo.php line 222
Sets a static header if it has not been set yet, and then sends the given commands as JSON-streamed data to the client, followed by flushing the output buffers.
static $header;
$header ??= first(true, cli || headers_sent() || [http_response_code(206), header('Content-Type: text/event-stream'), header('Cache-Control: no-store'), header('X-Content-Type-Options: nosniff'), phlo('app')->streaming = true]);
echo json_encode($cmds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).lf;
cli || [@ob_flush(), flush()];
function

create(iterable $items, Closure $keyCb, ?Closure $valueCb = null):array

/phlo/phlo.php line 272
This function combines the results of two iterations: one with the key callback and one with the value callback (or the items themselves if no value callback is specified), and creates an associative array from them.
return array_combine(loop($items, $keyCb), $valueCb ? loop($items, $valueCb) : $items);
function

debug(?string $msg = null)

/phlo/phlo.php line 273
Adds the message to a static debug array if the debug mode is enabled or disabled; returns the entire debug collection if no message is provided.
if (!debug) return;
static $debug = [];
if (is_null($msg)) return $debug;
$debug[] = $msg;
function

dirs(string $path):array|false

/phlo/phlo.php line 279
This code returns an array of directory paths that start with the specified `$path`, including the trailing slash for each directory.
return glob("$path*", GLOB_MARK | GLOB_ONLYDIR);
function

DOM(string $body = void, string $head = void, string $lang = 'en', string $bodyAttrs = void):string

/phlo/phlo.php line 228
This node generates a complete HTML page with doctype, html tag with language, head section, and body section, with the content dynamically filled in via parameters.
return "<!DOCTYPE html>\n<html lang=\"$lang\">\n<head>\n$head</head>\n<body$bodyAttrs>\n$body\n</body>\n</html>";
function

duration(int $decimals = 5, bool $float = false):string|float

/phlo/phlo.php line 280
Calculates the duration since the start of the request and displays it with the specified number of decimal places; optionally, the result is rounded and a speed indicator is added for short durations.
return last($d = microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true)), $float ? round($d, $decimals) : rtrim(rtrim(sprintf("%.{$decimals}f", $d), '0'), dot).'s'.($d > 0 && $d < .5 ? ' ('.round(1 / $d).'/s)' : void));
function

error(string $msg, int $code = 500):never

/phlo/phlo.php line 282
Make an exception with a specific message and optionally a code, default to 500.
throw new PhloException($msg, $code);
function

esc(string $string):string

/phlo/phlo.php line 281
This ensures that special HTML characters in the string are encoded so that they are displayed safely in an HTML context, including quotes and replacing invalid characters.
return htmlspecialchars((string)$string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
function

files(string|array $paths, string $ext = '*.*'):array

/phlo/phlo.php line 283
Find all files with the specified extension in the given paths and combine the results into one array.
return array_merge(...loop((array)$paths, fn($path) => glob("$path$ext")));
function

first(...$args):mixed

/phlo/phlo.php line 284
Gets the first element from the list of arguments.
return current($args);
function

HTTP(string $url, array $headers = [], bool $JSON = false, $POST = null, $PUT = null, $PATCH = null, bool $DELETE = false, ?string $agent = null):string|false

/phlo/phlo.php line 247
Processes the input parameters to build a cURL request with the correct HTTP method, headers, body, and agent. Executes the request and returns the response.
$curl = curl_init($url);
if ($POST || $PUT || $PATCH){
	if (!is_null($POST)) [$method = 'POST', $content = $POST];
	elseif (!is_null($PUT)) [$method = 'PUT', $content = $PUT];
	elseif (!is_null($PATCH)) [$method = 'PATCH', $content = $PATCH];
	curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
	if ($JSON) [!is_string($content) && $content = json_encode($content), array_push($headers, 'Content-Type: application/json', 'Content-Length: '.strlen($content))];
	curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
}
elseif ($DELETE) curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
$agent && curl_setopt($curl, CURLOPT_USERAGENT, $agent === true ? $_SERVER['HTTP_USER_AGENT'] : $agent);
curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, CURLOPT_ENCODING => void]);
$res = curl_exec($curl);
curl_close($curl);
return $res;
function

indent(string $string, int $depth = 1):string

/phlo/phlo.php line 285
Performs text indentation by repeatedly adding tab characters based on the depth, and ensures that existing lines are correctly aligned without extra spaces at the end.
return ($tab = str_repeat(tab, $depth)).rtrim(strtr($string, [lf => lf.$tab]), tab);
function

indentView(string $string, int $depth = 1):string

/phlo/phlo.php line 286
Adds indentations to a string based on depth, with extra tab indentations for new lines starting with '<'.
return last($tab = str_repeat(tab, $depth), rtrim(preg_replace('/\n(\t*)</', "\n$1$tab<", $string), tab));
function

json_read(string $file, ?bool $assoc = null):mixed

/phlo/phlo.php line 244
Loads the content of the specified file, decodes the JSON data, and returns it. If decoding fails or the file cannot be read, an error message is generated.
return json_decode(file_get_contents($file), $assoc) ?? error('Error reading '.esc($file));
function

json_write(string $file, $data, $flags = null):int|false

/phlo/phlo.php line 245
Write the JSON-encoded data to a file with an exclusive lock to ensure the data is stored securely.
return file_put_contents($file, json_encode($data, $flags ?? jsonFlags), LOCK_EX);
function

last(...$args):mixed

/phlo/phlo.php line 287
Returns the last element of the given arguments.
return end($args);
function

location(?string $location = null):never

/phlo/phlo.php line 288
This code defines an asynchronous function that, depending on whether a location is specified, redirects the user to that location or back to the previous page. If no location is provided, a default path is used.
async ? apply(location: $location ?? true) : [header('Location: '.($location ?? ($_SERVER['HTTP_REFERER'] ?? slash))), exit];
function

loop(iterable $data, closure|array $cb, ?string $implode = null):mixed

/phlo/phlo.php line 289
This method applies a callback to each element of an iterable, where the callback can consist of an array with an object and a method or just a closure. The result is an array with the processed values, or a concatenated string if an implode parameter is provided.
$return = [];
$isArray = is_array($cb);
foreach ($data AS $key => $value) $return[$key] = $isArray ? $cb[0]->{$cb[1]}($value, $key) : $cb($value, $key);
return is_null($implode) ? $return : implode($implode, $return);
function

mime(string $filename):string

/phlo/phlo.php line 295
Returns the MIME type based on the file extension from the name, or via mime_content_type if the file exists; otherwise defaults to 'application/octet-stream'.
return ['html' => 'text/html', 'css' => 'text/css', 'gif' => 'image/gif', 'ico' => 'image/x-icon', 'ini' => 'text/plain', 'js' => 'application/javascript', 'json' => 'application/json', 'jpg' => 'image/jpeg', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jfif' => 'image/jpeg', 'ogg' => 'audio/ogg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'pdf' => 'application/pdf', 'phlo' => 'application/phlo', 'php' => 'application/x-httpd-php', 'png' => 'image/png', 'svg' => 'image/svg+xml', 'txt' => 'text/plain', 'webp' => 'image/webp'][pathinfo($filename, PATHINFO_EXTENSION)] ?? (is_file($filename) ? mime_content_type($filename) : 'application/octet-stream');
function

obj(...$data):obj

/phlo/phlo.php line 296
Creates a new object with the given data as constructor parameters.
return new obj(...$data);
function

output(?string $content = null, ?string $filename = null, ?bool $attachment = null, ?string $file = null):never

/phlo/phlo.php line 229
Sets the HTTP headers for content-type, content-length, and content-disposition (attachment or inline), and then sends the content (file or string) to the output before the script stops.
header('Content-Type: '.mime($filename ?? basename($file ?? req)));
header('Content-Length: '.($file ? filesize($file) : strlen($content)));
if (is_bool($attachment) || $filename) header('Content-Disposition: '.($attachment ? 'attachment' : 'inline').';filename='.rawurlencode($filename ?? basename($file ?? req)));
$file ? readfile($file) : print($content);
exit;
function

phlo(?string $phloName = null, ...$args):mixed

/phlo/phlo.php line 142
This function manages and returns objects of a certain name, supported by a cache. If no name is provided, it returns the list of available names. For a specific name, it checks whether a special handle method exists and calls it, or creates a new object and stores it in the cache. Then, if present, the controller method is called before the object is returned.
static $list = [];
if (is_null($phloName)) return array_keys($list);
$phloName = strtr($phloName, [slash => us]);
$handle = method_exists($phloName, '__handle') ? $phloName::__handle(...$args) : ($args ? null : $phloName);
if ($handle === true){
	if (isset($list[$phloName])) return $list[$phloName]->objImport(...$args);
	$handle = $phloName;
}
elseif ($handle && isset($list[$handle])) return $list[$handle];
$phlo = new $phloName(...$args);
if ($handle) $list[$handle] = $phlo;
if ($phlo->hasMethod('controller') && (!cli || $phloName !== 'app')) $phlo->controller();
return $phlo;
function

phlo_app(...$args)

/phlo/phlo.php line 112
Initializes default values for build, debug, data, php, and www based on input parameters; defines constant values and configures class autoloading via a class map and fallback to file paths; loads dependencies such as Composer autoload; sets up a required redirect and build check; registers error and exception handlers; executes the 'app' node and handles CLI output or exceptions.
$args['build'] ??= false;
$args['debug'] ??= false;
$args['data'] ??= "$args[app]data/";
$args['php'] ??= $args['app'].($args['build'] ?? null ? 'php/' : void);
$args['www'] ??= "$args[app]www/";
foreach ($args AS $key => $value) define($key, $value);
spl_autoload_register(static function($class){
	static $map;
	$map ??= is_file($file = php.'classmap.php') ? require($file) : [];
	if (isset($map[$class])) return require(php.$map[$class]);
	if ($map) return build && debug && ($lib = phlo_find_lib('class', $class)) && phlo_activate_lib($lib);
	if (is_file($file = php.strtr($class, [us => dot]).'.php')) return require($file);
});
defined('composer') && require_once(composer.'vendor/autoload.php');
define('req', cli ? implode(slash, $cli = array_slice($_SERVER['argv'], 1)) : rawurldecode(substr(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), 1)));
debug && require(__DIR__.'/debug.php');
build && phlo_build_check() && (build['auto'] ?? true) && [require_once(__DIR__.'/build.php'), phlo_build()];
set_error_handler(static function($severity, $message, $file = null, $line = 0){
	if (!(error_reporting() & $severity)) return false;
	throw new PhloException($message, $severity, ['file' => $file, 'line' => $line]);
});
set_exception_handler('phlo_exception');
try {
	phlo('app');
	cli && print((strpos($cb = array_shift($cli), '::') ? $cb(...$cli) : [phlo($cb), array_shift($cli)](...$cli)).lf);
}
catch (Throwable $e){ phlo_exception($e); }
function

phlo_app_jsonfile(string $app, string $file)

/phlo/phlo.php line 111
Loads the contents of a file, replaces a specific string to adjust the path, decodes the JSON data, and then calls a function with this data and the application name.
phlo_app(...json_decode(strtr(file_get_contents($file), ['"%app/' => dq.$app]), true), app: $app);
function

phlo_async(string $obj, string $call, ...$args):bool

/phlo/phlo.php line 160
Performs an asynchronous operation by calling an external 'phlo_exec' function with the given object name, call name, and any additional arguments, without waiting for the result.
return phlo_exec(www, $obj, $call, false, ...$args);
function

phlo_build_check():bool

/phlo/phlo.php line 162
This code checks whether the file 'app.php' does not exist or if the last modified time of this file is older than the most recent modification time of the sources in 'phlo_sources()'.
return !is_file($app = php.'app.php') || filemtime($app) < array_reduce(phlo_sources(), fn($a, $f) => max($a, @filemtime($f)), 0);
function

phlo_error_log(string $path, string $msg):int|false

/phlo/phlo.php line 95
Adds an error message to a JSON log file by generating a unique ID based on the path and message; keeps track of how many times the same error occurs, and records the last time the error occurred.
$file = data.'errors.json';
$now = date('j-n-Y G:i:s');
$id = md5($path.preg_replace('/\s+/', void, trim(preg_replace('~(?:[A-Za-z]:)?[\\/](?:[^\s:/\\\\]+[\\/])*(?:([^\s:/\\\\]+\.[A-Za-z0-9]{1,8})|[^\s:/\\\\]+)(?::\d+)?~', '$1', $msg))));
$map = is_file($file) ? (json_read($file, true) ?: []) : [];
$row = $map[$id] ?? [];
$row['file'] = $path;
$row['req'] = req;
$row['msg'] = $msg;
$row['count'] = ($map[$id]['count'] ?? 0) + 1;
$row['lastOccured'] = $now;
unset($map[$id]);
$map = [...[$id => $row], ...$map];
return json_write($file, $map);
function

phlo_exception(Throwable $e):never

/phlo/phlo.php line 67
Error handling function that processes a Throwable by logging the message, conditions for debugging and development environment, and then generating the appropriate output (console, API, or HTML page) based on the context.
$msg = $e->getMessage();
static $retried = false;
if (build && debug && preg_match('/^Call to undefined function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $msg, $m) && !$retried && ($lib = phlo_find_lib('function', $m[1])) && phlo_activate_lib($lib)) location(slash.req);
$code = (int)$e->getCode();
$payload = $e instanceof PhloException ? $e->payload() : ['error' => $msg, 'code' => ($code ?: 500), 'type' => get_class($e), 'data' => ['file' => $e->getFile(), 'line' => $e->getLine()]];
if (phlo('app')->hasMethod('errorPage')) phlo('app')->errorPage($msg, (int)($payload['code'] ?? 500));
$d = is_array($payload['data'] ?? null) ? $payload['data'] : [];
$file = $d['file'] ?? $e->getFile();
$line = (int)($d['line'] ?? $e->getLine());
$short = shortpath($file).colon.$line;
phlo_error_log($short, $msg);
if (debug) debug_error($e);
if (cli || async){
	$text = ($payload['type'] ?? 'Error').colon.space.$msg;
	if (async) apply(error: $text);
	fwrite(STDERR, $text.lf);
	exit(1);
}
http_response_code($code = $payload['code'] ?? 500);
header('X-Content-Type-Options: nosniff');
$title = "Phlo $code Error";
$CSS = 'body{background:black;color:white;font-family:sans-serif;text-align:center;margin-top:18dvh}pre{white-space:pre-wrap}';
$body = '<h1>'.esc($title).'</h1><pre>'.esc(($payload['type'] ?? 'Error').colon.space.$msg).'</pre>';
print(DOM($body, tag('title', $title).lf.tag('style', $CSS)));
exit(1);
function

phlo_exec(string $path, string $obj, string $call, bool $sync = true, ...$args):string|bool

/phlo/phlo.php line 158
Executes a PHP script via the command line with specified parameters, and returns the output or executes the command asynchronously without output.
return last(exec('/usr/bin/php '.escapeshellarg(rtrim($path, slash).'/app.php').' '.escapeshellarg($obj).space.escapeshellarg($call).loop($args, fn($arg) => space.escapeshellarg((string)$arg), void).($sync ? void : ' > /dev/null 2>&1 &'), $res), $sync ? implode(lf, $res) : true);
function

phlo_exists(string $obj):bool

/phlo/phlo.php line 161
Check if a PHP file exists that matches the name by replacing the dot and then checking for the existence of the file.
return is_file(php.strtr($obj, [us => dot]).'.php');
function

phlo_sources():array

/phlo/phlo.php line 163
Collects .phlo files from a specified source folder or app folder, then adds extra library files if they exist, sorts the list case-insensitively, and returns the ordered list.
$sources = files(isset(build['sources']) ? build['sources'] : app, '*.phlo');
foreach (build['libs'] AS $lib) $sources[] = is_file($file = __DIR__."/libs/$lib.phlo") ? $file : error('Build Error: Library not found '.esc($lib));
natcasesort($sources);
return $sources;
function

phlo_sync(string $obj, string $call, ...$args):string

/phlo/phlo.php line 159
This is a function that calls another function or method with a specified object and call, using a global output function.
return phlo_exec(www, $obj, $call, true, ...$args);
function

regex(string $pattern, string $subject, int $flags = 0, int $offset = 0):array

/phlo/phlo.php line 297
Executes a regular expression on a string and returns the matches as an array; returns an empty array if no match is found.
return preg_match($pattern, $subject, $match, $flags, $offset) ? $match : [];
function

regex_all(string $pattern, string $subject, int $flags = 0, int $offset = 0):array

/phlo/phlo.php line 298
Executes a regular expression on the text and returns all found matches in an array; if no matches are found, an empty array is returned.
return preg_match_all($pattern, $subject, $matches, $flags, $offset) ? $matches : [];
function

req(int $index, ?int $length = null):mixed

/phlo/phlo.php line 299
Extracts a specific part from a URL by splitting the string, and returns that part. If a length is specified, multiple parts are concatenated into one string.
static $parts;
$parts ??= explode(slash, req);
return is_null($length) ? ($parts[$index] ?? null) : (implode(slash, array_slice($parts, $index, $length < 0 ? null : $length)) ?: null);
function

route(?string $method = null, string $path = void, ?bool $async = null, ?string $data = null, ?string $cb = null)

/phlo/phlo.php line 304
The code validates and processes an incoming request based on method, asynchronous flag, payload data, and URL path segments. It filters the request, extracts variables from the path, checks for patterns such as parameters with default values, fixed values, or defined lists, and collects these into arguments for a callback. If all checks pass, the callback is invoked with the collected arguments.
if ($method && $method !== method) return;
if (!is_null($async) && $async !== async) return;
if ($data && phlo('payload')->objKeys !== explode(comma, $data)) return;
$req = array_filter(explode(slash, req));
$cbArgs = [];
$index = -1;
foreach (array_filter(explode(space, $path)) AS $index => $item){
	$reqItem = req($index);
	if (strpos($item, '$') === 0){
		$item = substr($item, 1);
		if (str_ends_with($item, '=*')){
			$cbArgs[substr($item, 0, -2)] = implode(slash, array_slice($req, $index));
			$index = count($req) - 1;
			break;
		}
		elseif (str_ends_with($item, qm)){
			$item = substr($item, 0, -1);
			if ($reqItem && $item !== $reqItem) return;
			$reqItem = $item === $reqItem;
		}
		elseif (str_contains($item, eq)){
			list ($item, $default) = explode(eq, $item, 2);
			$default = $default ?: null;
		}
		elseif (is_null($reqItem)) return;
		if (str_contains($item, dot) && (list($item, $length) = explode(dot, $item, 2)) && strlen($reqItem) != $length) return false;
		if (str_contains($item, colon)){
			(list ($item, $list) = explode(colon, $item, 2)) && $list = explode(comma, $list);
			if (!$reqItem || in_array($reqItem, $list)) $cbArgs[$item] = $reqItem ?: $default ?? null;
			else return;
		}
		else $cbArgs[$item] = $reqItem ?? $default;
	}
	elseif ($item !== $reqItem) return;
}
if (isset($req[$index + 1])) return;
if (!$cb) return obj(...$cbArgs);
if ($cb(...$cbArgs) === false) return;
exit;
function

shortpath(?string $file):string

/phlo/phlo.php line 345
It retrieves the second to last used part and the last part of a path, separated by a slash, unless the path has fewer than two parts, then only the last part is returned. For empty input, 'unknown' is returned.
if (!$file) return 'unknown';
$p = explode(slash, str_replace(bs, slash, $file));
$n = count($p);
return $n >= 2 ? $p[$n - 2].slash.$p[$n - 1] : end($p);
function

size_human(int $size, int $precision = 0):string

/phlo/phlo.php line 351
This function converts a size in bytes to a human-readable format by dividing the number by 1024 until it falls within the appropriate range, then appends the correct scale (B, KB, MB, GB, TB) with the specified precision.
foreach (['b', 'Kb', 'Mb', 'Gb', 'Tb'] AS $range){
	if ($size / 1024 < 1) break;
	$size /= 1024;
}
return round($size, $precision).$range;
function

slug(string $text):string

/phlo/phlo.php line 358
This code converts text into a URL-friendly slug: it transforms text to lowercase, removes accents, replaces non-alphanumeric characters with hyphens, and removes unnecessary hyphens from both ends.
return trim(preg_replace('/[^a-z0-9]+/', dash, strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text))), dash);
function

tag(string $tagName, ?string $inner = null, ...$args):string

/phlo/phlo.php line 236
Creates an HTML tag with the specified tag name and optional content, and adds non-NULL extra attributes as tag or attribute values.
return "<$tagName".loop(array_filter($args, fn($value) => !is_null($value)), fn($value, $key) => space.strtr($key, [us => dash]).($value === true ? void : '="'.esc($value).'"'), void).'>'.(is_null($inner) ? void : "$inner</$tagName>");
function

time_human(?int $time = null):string

/phlo/phlo.php line 359
This code calculates a human-readable time display by comparing a given timestamp with the current time and converting it into an appropriate time format (such as days, weeks, months). It uses a list of labels and corresponding multipliers to scale the age into a comprehensible unit, and stops when the age falls below a certain threshold.
static $labels;
$labels ??= last($labels = arr(seconds: 60, minutes: 60, hours: 24, days: 7, weeks: 4, months: 13, years: 1), defined('tsLabels') && $labels = array_combine(tsLabels, $labels), $labels);
$age = time() - $time;
foreach ($labels AS $range => $multiplier){
	if ($age / $multiplier < 1.6583) break;
	$age /= $multiplier;
}
return round($age)." $range";
function

title(?string $title = null, string $implode = ' - '):string

/phlo/phlo.php line 237
Provides a static list of titles, adds a given title if present, or uses the current app title or a default value, and combines the list into a single string with a specified separator.
static $titles = [];
if ($title) return $titles[] = $title;
$titles[] = phlo('app')->title ?: 'Phlo '.phlo;
return implode($implode, $titles);
function

token(int $length = 8, ?string $input = null, ?string $sha1 = null):string

/phlo/phlo.php line 369
Generates a random token of a specified length by first creating a SHA1 hash based on input, a number, or a random number, and then concatenating letters based on the hash values until the desired length is reached.
$sha1 ??= sha1($input ?? random_int(date('Y'), PHP_INT_MAX), true);
$token = void;
for ($i = 0; strlen($token) < $length; $i++) $token .= chr(ord('a') + (ord($sha1[$i % 20]) % 26));
return $token;
function

view(?string $body = null, ?string $title = null, array|string $css = [], array|string $js = [], array|string $defer = [], array|string $options = [], array $settings = [], ?string $ns = null, bool|string $uri = req, ...$cmds):void

/phlo/phlo.php line 170
This node handles the generation of an HTML page and the configuration of resources and metadata. It adds CSS and JS files, sets meta-titles and other head elements, manages preloading and defer options, and provides the HTML structure including body attributes. In async processing, important content is passed and applied via `$cmds`.
if (cli) return;
!async && !is_bool($uri) && $uri !== req && location("/$uri");
$app = phlo('app');
$title && title($title);
$css = array_merge((array)$css, (array)$app->css);
$js = array_merge((array)$js, (array)$app->js);
$defer = array_merge((array)$defer, (array)$app->defer);
$options = implode(space, array_merge((array)$options, (array)$app->options, debug ? ['debug'] : []));
$settings = array_merge($settings, (array)$app->settings);
if (async){
	$uri !== false && $cmds['uri'] = $uri;
	$cmds['trans'] ??= true;
	$cmds['title'] = title();
	$css && $cmds['css'] = $css;
	$js && $cmds['js'] = $js;
	$defer && $cmds['defer'] = $defer;
	$cmds['options'] = $options;
	$cmds['settings'] = $settings;
	!is_null($body) && $cmds['inner']['body'] = $body;
	apply(...$cmds);
}
$body ??= $cmds['main'] ?? void;
debug && $body .= lf.debug_render();
$ns ??= $app->ns ?? 'app';
$link = $app->link ?: [];
$head = tag('title', inner: title()).lf;
$head .= '<meta name="viewport" content="'.($cmds['viewport'] ?? $app->viewport ?? 'width=device-width').'">'.lf;
$app->description && $head .= "<meta name=\"description\" content=\"$app->description\">\n";
$app->themeColor && $head .= "<meta name=\"theme-color\" content=\"$app->themeColor\">\n";
$app->image && $head .= "<meta property=\"og:image\" content=\"$app->image\">\n";
is_file(www.$filename = 'favicon.ico') && $head .= "<link rel=\"favicon\" href=\"/$filename?".version."\">\n";
is_file(www.$filename = 'manifest.json') && $head .= "<link rel=\"manifest\" href=\"/$filename?".version."\">\n";
is_file(www.$filename = 'icons.png') && $link[] = "</$filename?".version.">; rel=preload; as=image";
$app->head && $head .= $app->head.lf;
foreach ($css AS $item) $head .= '<link rel="stylesheet" href="'.esc($item).'">'.lf;
is_file(www.$filename = "$ns.css") && [$link[] = "</$filename?".version.">; rel=preload; as=style", $head .= '<link rel="stylesheet" href="'.esc(slash.$filename.qm.version).'">'.lf];
foreach ($js AS $item) $head .= '<script src="'.esc($item).'"></script>'.lf;
foreach ($defer AS $item) $head .= '<script src="'.esc($item).'" defer></script>'.lf;
is_file(www.$filename = "$ns.js")  && [$link[] = "</$ns.js?".version.">; rel=preload; as=script", $head .= '<script src="'.esc(slash.$filename.qm.version).'" defer></script>'.lf];
!build && $link && header('Link: '.implode(comma, $link), false);
if ($lang = $cmds['lang'] ?? $app->lang ?? 'en') unset($cmds['lang']);
$bodyAttrs = void;
$options && $bodyAttrs .= " class=\"$options\"";
$settings && $bodyAttrs .= loop($settings, fn($value, $key) => ' data-'.$key.'="'.esc($value).'"', void);
die(DOM($body, $head, $lang, $bodyAttrs));