diff --git a/Documentation-develop/CONFIG.md b/Documentation-develop/CONFIG.md
index 27f440c5bbf8d493c87546244f564348c8701404..b71f07f1b81d3839ee5bd16fde6744276de914aa 100644
--- a/Documentation-develop/CONFIG.md
+++ b/Documentation-develop/CONFIG.md
@@ -19,17 +19,109 @@ IMATHUZH\Qfq\Core\Store\Store::fillStoreSystem()
 IMATHUZH\Qfq\Core\Store\Config::getConfigArray()
 IMATHUZH\Qfq\Core\Store\Config::readConfig()
 
-To create a new QFQ Config option:
+How to create a new config option
+==================================
 
-ext_conf_template.txt:
+To create a new config option, you have to make the changes specified below in the following files.
 
-* create new entry
-* set a default value in ext_conf_template.txt
+ext_conf_template.txt
+---------------------
 
-config::setDefaults()
+The following variables must be set:
 
-* Define defaults if nothing is given
+**cat**
+(category where the new option will be located, in the extension configuration of your typo3 backend)
 
-DatabaseUpdate=>checkT3QfqConfig():
+**type** (datatype of the config option)
 
-* In case existing installations should get a new default during QFQ update. 
+possible datatypes:
+* boolean (checkbox)
+* color (colorpicker)
+* int (integer value)
+* int+ (positive integer value)
+* integer (integer value)
+* offset (offset)
+* options (option select)
+```type=options[label1=value1,label2=value2,value3];```
+* small (small text field)
+* string (text field)
+* user (user function)
+```type=user[Vendor\MyExtensionKey\ViewHelpers\MyConfigurationClass->render];```
+* wrap (wrap field)
+
+**label** (title and description of the config option, split by ":")
+
+**myVariable** (name the variable of the config option and assign a default value)
+
+**Example**
+
+```
+# cat=config/config; type=boolean; label=MyLabel:Description
+myVariable = value1
+```
+
+Constants.php
+-------------
+
+Best practice would be defining constants with the name of your variable,
+since this name should never be changed.
+
+```
+const SYSTEM_MY_VARIABLE = 'myVariable';
+const F_MY_VARIABLE = 'SYSTEM_MY_VARIABLE';
+const FE_MY_VARIABLE = 'SYSTEM_MY_VARIABLE';
+```
+
+
+Config.php
+---------------------
+
+In the function **setDefaults()** a default value should be set.
+</br>Important in case of new variables: new variables do not exist in QFQ extension config and do not get the default defined in ext_conf_template.txt
+
+```
+default = [
+    ...
+    SYSTEM_MY_VARIABLE => 'true',
+    ...
+];
+```
+
+Support.php
+----------
+
+To set the default value of a FormElement you can use the **setFeDefaults()** function.
+</br>Wich provides the default value for the FormElement using the **system store**.
+</br>The **system store** contains all the variables defined in the typo3 extension configuration.
+
+```
+self::setIfNotSet($formElement, FE_MY_VARIABLE, $store->getVar(SYSTEM_MY_VARIABLE, STORE_SYSTEM));
+```
+
+StoreTest.php
+-------------
+
+The expected default value must be specified in the **testConfigIniDefaultValues()** function so that the unit test can run without errors.
+
+```
+$expect = [
+    ...
+    SYSTEM_MY_VARIABLE => 'true',
+    ...
+];
+```
+
+How to handle variables
+--------------------------------------
+
+Here is an example on how you would go about handling variables that are defined on all levels (SYSTEM -> Form -> FormElement)
+
+```
+$myVar = $store->getVar(SYSTEM_MY_VARIABLE, STORE_SYSTEM);
+if(isset($this->formSpec[F_MY_VARIABLE])){
+    $myVar = $this->formSpec[F_MY_VARIABLE];
+}
+if(isset($this->formElement[FE_MY_VARIABLE])){
+    $myVar = $this->formElement[FE_MY_VARIABLE];
+}
+```
diff --git a/Documentation-develop/PARSER.md b/Documentation-develop/PARSER.md
new file mode 100644
index 0000000000000000000000000000000000000000..6303f165397742e861471067ebdbbb9c2f2cd03b
--- /dev/null
+++ b/Documentation-develop/PARSER.md
@@ -0,0 +1,119 @@
+# Parsers and Tokenizer
+
+## Motivation and use cases
+
+Parsing values for special QFQ columns starting from simple lists
+of key-value pairs to enhanced JSON strings.
+
+## Overview of classes
+
+All classes are defined in the namespace `IMATHUZH\Qfq\Core\Parser`.
+
+### StringTokenizer
+
+This class provides a generator that iterates over a string
+and returns tokens bound by predefined delimiters. The delimiters
+are search in a smart way:
+* delimiters escaped with a backslash are ignored in the search
+* the parser can distinguish between escaping and escaped backslashes,
+  i.e. the colon (as a delimiter) is ignored in the string `ab\:cd`
+  but not in `ab\\:cd`
+* a part of a string between quotes is treated as a plain text - all delimiters
+  are ignored (and the quote characters are removed).
+
+#### Examples with delimiters `:,|`:
+
+| Input string      | Resulting sequence of tokens |
+|-------------------|------------------------------|
+| `ab:cd,ef\|gh`    | `'ab' 'cd' 'ef' 'gh'`        |
+| `"ab:cd",ef\\|gh` | `'ab:cd' 'ef\|gh'`           |
+
+#### Usage
+<pre><code class="php">$tokenizer = new StringTokenizer(':,|');
+foreach ($tokenizer->tokenized('ab:cd,ef\|gh') as list($token, $delimiter)) {
+   // $token is an instance of Token class:
+   //     $token->value    is a string representation of the token
+   //     $token->isString is true if the token is a string (quotes were used)
+   //     $token->empty()  is true for a token generated only from whitespace characters
+   // $delimiter === null    when the end of the string is reached
+}
+</code></pre>
+
+### SimpleParser
+
+This class parses a string into a list of tokens separated by delimiters.
+Comparing to `StringTokenizer`, the returned tokens literal values or special objects
+the processing can be tweaked by options provided as an array in the second parameter.
+
+| Parameters key         | Type | Meaning                                                                |
+|------------------------|------|------------------------------------------------------------------------|
+| `OPTION_PARSE_NUMBERS` | bool | Convert tokens to numbers when possible                                |
+| `OPTION_KEEP_SIGN`     | bool | Creates an instance of `SignedNumber` if a number has an explicit sign |
+| `OPTION_KEY_IS_VALUE`  | bool | Keys with no values are assigned its name as the value                 |
+| `OPTION_EMPTY`         | any  | The value used for empty tokens                                        |
+
+Note that the option `OPTION_KEY_IS_VALUE` is not used by `SimpleParser` but it is used
+by derived classes.
+
+**Note**: the option `OPTION_KEEP_SIGN` is used by `jwt` column, so that claims
+`exp` and `nbf` can be specified either with absolute (no plus) or relative
+(with a plus) timestamps.
+
+#### Usage
+
+<pre><code class="php">$parser = new SimpleParser(":|");
+// By default five special values are configured:
+//    'null' -> null
+//    'true', 'yes' -> true
+//    'false', 'no' -> false
+// More can be defined by updating $specialValues property:
+$parser->specialValues['qfq'] = 'QFQ is great';
+
+// This returns an array ['abc', 'efg', 123, true, 'QFQ is great']
+$parser->parse("abc:efg|123|yes:qfq");
+
+// The tokens can be iterated as follows
+foreach($parser->iterate("abc:efg|123|yes") as $token) {
+   ...
+}
+</code></pre>
+
+### KVPairListParser
+
+This class parses a list of key-value pairs into an associative array.
+It requires two arguments: the list separator and key-value separator.
+
+#### Usage
+<pre><code class="php">// Default separators are , and :
+$parser = new KVPairListParser("|", "=");
+$parser->parse("a=43|b=false|xyz='a|b'");
+// result: [ 'a' => 43, 'b' => false, 'xyz' => 'a|b' ]
+
+foreach ($parser->iterate("a=43|b=false|xyz='a|b'") as $key => $value) {
+   ...
+}
+</code></pre>
+
+### MixedTypeParser
+
+This parser understands both lists and dictionaries and both structures can be nested.
+The constructor must be provided six delimiters in one string: list separator,
+key-value separator, list delimiters (begin and end), and dictionary delimiters
+(begin and end). The default value is `,:[]{}`. It is also possible to replace
+the list and dictionary delimiters with spaces, in which case the parser will
+ignore it. For instance
+* `new MixedTypeParser(',:[]')` can parse nested lists, but not dictionaries (the string is padded)
+* `new MixedTypeParser(',:  {}')` can parse nested dictionaries, but not lists
+
+This parser can be seen as an extension to a JSON parser: strings does not have
+to be enclosed with quotes.
+
+#### Usage
+
+<pre><code class="php">$parser = new MixedTypeParser(',:[]{}', [ /* options */ ]);
+$parser->parse('[0, { a: 14, b: 16 }, abc]');
+$parser->parseList('abc, [x, y, z], {a:15}, xyz');
+$parser->parseDictionary('num:15, arr:[x, y, z], dict:{a:15}, str:xyz');
+</code></pre>
+
+**Note**: there is no meaningful `iterate()` method.
diff --git a/Documentation/Concept.rst b/Documentation/Concept.rst
index 7d3bf589c9200975486ae8b2c7ded999d4671af1..d6d7dbde60ef0247c61951b4a04e9c5a8b63c871 100644
--- a/Documentation/Concept.rst
+++ b/Documentation/Concept.rst
@@ -89,98 +89,100 @@ QFQ Keywords (Bodytext)
 
 **All of these parameters are optional.**
 
-+-------------------------+---------------------------------------------------------------------------------+
-| Name                    | Explanation                                                                     |
-+=========================+=================================================================================+
-| form                    | | Formname.                                                                     |
-|                         | | Static: **form = person**                                                     |
-|                         | | By SIP: **form = {{form:SE}}**                                                |
-|                         | | By SQL: **form = {{SELECT c.form FROM Config AS c WHERE c.id={{a:C}} }}**     |
-+-------------------------+---------------------------------------------------------------------------------+
-| r                       | | <record id>. The form will load the record with the specified id.             |
-|                         | | Static: **r = 123**                                                           |
-|                         | | By SQL: **r = {{SELECT ...}}**                                                |
-|                         | | If not specified, the SIP parameter 'r' is used.                              |
-+-------------------------+---------------------------------------------------------------------------------+
-| dbIndex                 | E.g. `dbIndex = {{indexQfq:Y}}` Select a DB index. Only necessary if a          |
-|                         | different than the standard DB should be used.                                  |
-+-------------------------+---------------------------------------------------------------------------------+
-| debugShowBodyText       | If='1' and :ref:`configuration`:*showDebugInfo: yes*, shows a                   |
-|                         | tooltip with bodytext                                                           |
-+-------------------------+---------------------------------------------------------------------------------+
-| sqlLog                  | Overwrites :ref:`configuration`: :ref:`SQL_LOG` . Only affects `Report`,        |
-|                         | not `Form`.                                                                     |
-+-------------------------+---------------------------------------------------------------------------------+
-| sqlLogMode              | Overwrites :ref:`configuration`: :ref:`SQL_LOG_MODE<SQL_LOG_MODE>` .            |
-|                         | Only affects `Report`, not `Form`.                                              |
-+-------------------------+---------------------------------------------------------------------------------+
-| render                  | See :ref:`report-render`. Overwrites :ref:`configuration`: render.              |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.fbeg            | Start token for every field (=column)                                           |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.fend            | End token for every field (=column)                                             |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.fsep            | Separator token between fields (=columns)                                       |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.fskipwrap       | Skip wrapping (via fbeg, fsep, fend) of named columns. Comma separated list of  |
-|                         | column id's (starting at 1). See also the special column name '_noWrap' to      |
-|                         | suppress wrapping.                                                              |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.shead           | Static start token for whole <level>, independent if records are selected       |
-|                         | Shown before `head`.                                                            |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.stail           | Static end token for whole <level>, independent if records are selected.        |
-|                         | Shown after `tail`.                                                             |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.head            | Dynamic start token for whole <level>. Only if at least one record is select.   |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.tail            | Dynamic end token for whole <level>. Only if at least one record is select.     |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.rbeg            | Start token for row.                                                            |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.rbgd            | Alternating (per row) token.                                                    |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.rend            | End token for row. Will be rendered **before** subsequent levels are processed  |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.renr            | End token for row. Will be rendered **after** subsequent levels are processed   |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.rsep            | Seperator token between rows                                                    |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.sql             | SQL Query                                                                       |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.twig            | Twig Template                                                                   |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.althead         | If <level>.sql has no rows selected (empty), these token will be rendered.      |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.altsql          | If <level>.sql has no rows selected (empty) or affected (delete, update, insert)|
-|                         | the <altsql> will be fired. Note: Sub queries of <level> are not fired, even if |
-|                         | <altsql> selects some rows.                                                     |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.content         | | *show* (default): content of current and sub level are directly shown.        |
-|                         | | *hide*: content of current and sub levels are **stored** and not shown.       |
-|                         | | *hideLevel*: content of current and sub levels are **stored** and only sub    |
-|                         | | levels are shown.                                                             |
-|                         | | *store*: content of current and sub levels are **stored** and shown.          |
-|                         | | To retrieve the content: `{{<level>.line.content}}`.                          |
-|                         | | See :ref:`syntax-of-report`                                                   |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.count      | Current row index. Will be replaced before the query is fired in case of        |
-|                         | ``<level>`` is an outer/previous level or it will be replaced after a query is  |
-|                         | fired in case ``<level>`` is the current level.                                 |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.total      | Total rows (MySQL ``num_rows`` for *SELECT* and *SHOW*, MySQL ``affected_rows`` |
-|                         | for *UPDATE* and *INSERT*.                                                      |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.insertId   | Last insert id for *INSERT*.                                                    |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.content    | Show content of `<level>` (content have to be stored via <level>.content=....)  |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.altCount   | Like 'line.count' but for 'alt' query.                                          |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.altTotal   | Like 'line.total' but for 'alt' query.                                          |
-+-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.altInsertId| Like 'line.insertId' but for 'alt' query.                                       |
-+-------------------------+---------------------------------------------------------------------------------+
++--------------------------------+---------------------------------------------------------------------------------+
+| Name                           | Explanation                                                                     |
++================================+=================================================================================+
+| form                           | | Formname.                                                                     |
+|                                | | Static: **form = person**                                                     |
+|                                | | By SIP: **form = {{form:SE}}**                                                |
+|                                | | By SQL: **form = {{SELECT c.form FROM Config AS c WHERE c.id={{a:C}} }}**     |
++--------------------------------+---------------------------------------------------------------------------------+
+| r                              | | <record id>. The form will load the record with the specified id.             |
+|                                | | Static: **r = 123**                                                           |
+|                                | | By SQL: **r = {{SELECT ...}}**                                                |
+|                                | | If not specified, the SIP parameter 'r' is used.                              |
++--------------------------------+---------------------------------------------------------------------------------+
+| dbIndex                        | E.g. `dbIndex = {{indexQfq:Y}}` Select a DB index. Only necessary if a          |
+|                                | different than the standard DB should be used.                                  |
++--------------------------------+---------------------------------------------------------------------------------+
+| debugShowBodyText              | If='1' and :ref:`configuration`:*showDebugInfo: yes*, shows a                   |
+|                                | tooltip with bodytext                                                           |
++--------------------------------+---------------------------------------------------------------------------------+
+| sqlLog                         | Overwrites :ref:`configuration`: :ref:`SQL_LOG` . Only affects `Report`,        |
+|                                | not `Form`.                                                                     |
++--------------------------------+---------------------------------------------------------------------------------+
+| sqlLogMode                     | Overwrites :ref:`configuration`: :ref:`SQL_LOG_MODE<SQL_LOG_MODE>` .            |
+|                                | Only affects `Report`, not `Form`.                                              |
++--------------------------------+---------------------------------------------------------------------------------+
+| render                         | See :ref:`report-render`. Overwrites :ref:`configuration`: render.              |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.fbeg                   | Start token for every field (=column)                                           |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.fend                   | End token for every field (=column)                                             |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.fsep                   | Separator token between fields (=columns)                                       |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.fskipwrap              | Skip wrapping (via fbeg, fsep, fend) of named columns. Comma separated list of  |
+|                                | column id's (starting at 1). See also the :ref:`special-column-names` '_noWrap' |
+|                                | to suppress wrapping.                                                           |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.shead                  | Static start token for whole <level>, independent if records are selected       |
+|                                | Shown before `head`.                                                            |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.stail                  | Static end token for whole <level>, independent if records are selected.        |
+|                                | Shown after `tail`.                                                             |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.head                   | Dynamic start token for whole <level>. Only if at least one record is select.   |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.tail                   | Dynamic end token for whole <level>. Only if at least one record is select.     |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.rbeg                   | Start token for row.                                                            |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.rbgd                   | Alternating (per row) token.                                                    |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.rend                   | End token for row. Will be rendered **before** subsequent levels are processed  |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.renr                   | End token for row. Will be rendered **after** subsequent levels are processed   |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.rsep                   | Seperator token between rows                                                    |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.sql                    | SQL Query                                                                       |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.twig                   | Twig Template                                                                   |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.althead                | If <level>.sql has no rows selected (empty), these token will be rendered.      |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.altsql                 | If <level>.sql has no rows selected (empty) or affected (delete, update, insert)|
+|                                | the <altsql> will be fired. Note: Sub queries of <level> are not fired, even if |
+|                                | <altsql> selects some rows.                                                     |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level>.content                | | *show* (default): content of current and sub level are directly shown.        |
+|                                | | *hide*: content of current and sub levels are **stored** and not shown.       |
+|                                | | *hideLevel*: content of current and sub levels are **stored** and only sub    |
+|                                | | levels are shown.                                                             |
+|                                | | *store*: content of current and sub levels are **stored** and shown.          |
+|                                | | To retrieve the content: `{{<level>.line.content}}`.                          |
+|                                | | See :ref:`syntax-of-report`                                                   |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.count       | Current row index. Will be replaced before the query is fired in case of        |
+|                                | ``<level>``/``<alias>`` is an outer/previous level or it will be replaced after |
+|                                | a query is fired in case ``<level>``/``<alias>`` is the current level.          |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.total       | Total rows (MySQL ``num_rows`` for *SELECT* and *SHOW*, MySQL ``affected_rows`` |
+|                                | for *UPDATE* and *INSERT*.                                                      |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.insertId    | Last insert id for *INSERT*.                                                    |
+| <alias>.line.insertId          |                                                                                 |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.content     | Show content of `<level>`/`<alias>` (content have to be stored via              |
+|                                | <level>.content=... or <alias>.content=...).                                    |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.altCount    | Like 'line.count' but for 'alt' query.                                          |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.altTotal    | Like 'line.total' but for 'alt' query.                                          |
++--------------------------------+---------------------------------------------------------------------------------+
+| <level|alias>.line.altInsertId | Like 'line.insertId' but for 'alt' query.                                       |
++--------------------------------+---------------------------------------------------------------------------------+
 
 .. _`report-render`:
 
diff --git a/Documentation/Form.rst b/Documentation/Form.rst
index 58b8c7feb951e77808c72918aaba6a43a39be5b4..52703cbd410dea3a02558565df532db6fdbe1e77 100644
--- a/Documentation/Form.rst
+++ b/Documentation/Form.rst
@@ -126,31 +126,41 @@ FormElement processing order:
 
 .. _record_locking:
 
+
 Record locking
 --------------
 
-Forms and 'record delete'-function support basic record locking. A user opens a form: starting with the first change of content, a
-record lock will be acquired in the background. If the lock is denied (e.g. another user already owns a lock on the record) an
-alert is shown.
-By default the record lock mode is 'exclusive' and the default timeout is 15 minutes. Both can be configured per form.
-The general timeout can also be configured in :ref:`configuration` (will be copied to the form during creating the form).
-
-The lock timeout counts from the first change, not from the last modification on the form.
-
-If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again.
+Support for record locking is given with mode:
 
-A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record.
+* *exclusive*: user can't force a write.
 
-If a `Form` acts on further records (e.g. via FE action), those further records are not protected by this basic record locking.
+  * Including a timeout (default 15 mins recordLockTimeoutSeconds in :ref:`configuration`) for maximum lock time.
 
-If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied.
+* *advisory*: user is only warned, but allowed to overwrite.
+* *none*: no bookkeeping about locks.
 
-If there are different locking modes in multiple forms, the most restricting mode applies for the current lock.
+Details:
+
+* For 'new' records (r=0) there is no locking at all.
+* The record locking protection is based on the `tablename` and the `record id`. Different `Forms`, with the same primary table,
+  will be protected by record locking.
+* Action-`FormElements` updating non primary table records are not
+  protected by 'record locking': the QFQ record locking is *NOT 100%*.
+* The 'record locking' mode will be specified per `Form`. If there are multiple Forms with different modes, and there is
+  already a lock for a `tablename` / `record id` pair, the most restrictive will be applied.
+* A user opens a form: starting with the first change of content, a record lock will be acquired in the background. If
+  the lock is denied (e.g. another user already owns a lock on the record) an alert is shown. This means: the lock timeout
+  counts from the first change, not from the last modification on the form.
+* If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again.
+* A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record.
+* If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied.
+* If there are different locking modes in multiple forms, the most restricting mode applies for the current lock.
+* If the same user opens the same recording in different tabs or browsers, the user has the possibility to skip a lock.
 
 Exclusive
 ^^^^^^^^^
 
-An existing lock on a record forbids any write action on that record.
+An existing lock on a record forbids any write action on that record. Exception: locks owned by the same user might be overwritten.
 
 Advisory
 ^^^^^^^^
@@ -3183,30 +3193,6 @@ To automatically delete slave records, use a form and create `beforeDelete` Form
 
 You might also check the form 'form' how the slave records 'FormElement' will be deleted.
 
-.. _locking-record:
-
-Locking Record / Form
----------------------
-
-Support for record locking is given with mode:
-
-* *exclusive*: user can't force a write.
-
-  * Including a timeout (default 15 min recordLockTimeoutSeconds in :ref:`configuration`) for maximum lock time.
-
-* *advisory*: user is only warned, but allowed to overwrite.
-* *none*: no bookkeeping about locks.
-
-For 'new' records (r=0) there is no locking at all.
-
-The record locking protection is based on the `tablename` and the `record id`. Different `Forms`, with the same primary table,
-will be protected by record locking. On the other side, action-`FormElements` updating non primary table records are not
-protected by 'record locking': the QFQ record locking is *NOT 100%*.
-
-The 'record locking' mode will be specified per `Form`. If there are multiple Forms with different modes, and there is
-already a lock for a `tablename` / `record id` pair, the most restrictive will be applied.
-
-
 Best practice
 -------------
 
diff --git a/Documentation/Release.rst b/Documentation/Release.rst
index 051c77d211869f7dfde85be9cdddbab32f85c284..0203f61e86520496b7c6ce1f6d5a35f4d3d468ba 100644
--- a/Documentation/Release.rst
+++ b/Documentation/Release.rst
@@ -262,6 +262,214 @@ Bug Fixes
 * #15523 / Search/Refactor broken for Multi-DB.
 * #15626 / Multi-DB: FormEditor save error.
 
+Version 23.10.1
+---------------
+
+Date: 22.10.2023
+
+Notes
+^^^^^
+
+Features
+^^^^^^^^
+
+* #15098 / Implemented qfqFunction in QFQ variable for usage in forms.
+* Doc: Replace many places single back tick by double back tick. Add hint for 'Row size too large'.  Added Enis & Jan
+  as Developer. Add hint use mysqldump with one row per record.
+
+Bug Fixes
+^^^^^^^^^
+
+* #17003 / inline edit - dark mode has wrong css path.
+* #17075 / Fix broken '... AS _restClient'.
+* #17091 / upload_Incorrect_integer_value_fileSize.
+* #15795 / Upload: download button not shown after pressing save.
+* RTD: Fix broken readthedocs rendering.
+
+Version 23.10.0
+---------------
+
+Date: 05.10.2023
+
+Features
+^^^^^^^^
+
+* #16350 / QFQ Table 'FormSubmiLog': update recordid after insert.
+* #16350 / sql.log: reference to FormSubmitLog entry. All SQL statements generated by one HTTP Post (Form Submit) can
+  be identified.
+* #16350 / Do not log Dirty.
+* #16350 / If the T3 instance is behind a proxy, log HTTP_X_REAL_IP instead of REMOTE_ADDR in logfiles.
+* #16584 / FormEditor Report: Default without statistics.
+* #16589 / Implemented language configuration in backend for tt-content type qfq.
+* #16798 / Report inline edit v2. Improved search inside inline edit report. Whole content will be searched. Added
+  ability to switch the editor to dark mode.
+* Doc: Refactor description of {{random:V}}.
+* Doc: Add config option 'protectedFolderCheck'.
+
+Bug Fixes
+^^^^^^^^^
+
+* #16573 / Fixed wrong built date and datetime string if default value was given.
+* #16574 / Added multiple siteConfigurations compatibility for typo3 v10 and 11.
+* #16616 / Fixed typeahead api query response problem if typeahead sql is not used.
+* #16664 / Fix multi db user error.
+* #16975 / Fix problem if a 'Form Submit' contains more than 64kB data. This can happen easily for 'fabric' elements.
+
+Version 23.6.4
+--------------
+
+Date: 26.06.2023
+
+Bug Fixes
+^^^^^^^^^
+
+* #16485 / TypeAhead: strpos() string, array given error.
+* #16488 / Missing default values break saving records. New custom FE.parameter.defaultValue.
+* #16491 / FE Typ Upload - JS failure: document.querySelector() is null.
+
+Version 23.6.3
+--------------
+
+Date: 22.06.2023
+
+Bug Fixes
+^^^^^^^^^
+
+* #16478 / Rest API: Fixed stream_get_contents failure in PHP8.1 Generic Error.
+
+Version 23.6.2
+--------------
+
+Date: 21.06.2023
+
+Bug Fixes
+^^^^^^^^^
+
+* #16475 / Spontaneous spaces in HTML emails.
+* #16476 / SQL columns Text/Blog with default empty string becomes "''"
+
+
+Version 23.6.1
+--------------
+
+Date: 16.06.2023
+
+Notes
+^^^^^
+
+* QFQ is Typo3 V11 compatible
+
+Bug Fixes
+^^^^^^^^^
+
+* #16372 / Upload Element 'Undefined index htmlDownloadButton' und 'unknown mode ID'
+* #16381 / Form title dynamic update broken.
+* #16392 / Reevaluate sanitize class for each store.
+* Fix db column enum dropdown 'data truncated'
+* DB Update 'alter index' pre check if exists
+* FE: Upload - fix undefined index.
+
+Version 23.6.0
+--------------
+
+Date: 09.06.2023
+
+Features
+^^^^^^^^
+
+* Typo3 V11 compatible
+* #9579 / Multiform with Process Row.
+* #15770 / httpOrigin: Parameter missing in QFQ Config.
+* #16285 / PHP V8 Migration.
+* #16364 / T3 >=V10 pageSlug and ForwardPage.
+* Add .phpunit.result.cache to .gitignore.
+* Add bullet-orange.gif.
+* Add index createdFeUserFormId to table FormSubmitLog.
+* Doc: Add link to icons. Update use-case self-registration and add info in case of namesake. Unify word 'spam' to 'junk'.
+  Add bootstrap links.
+* Remove Form/FormElement parameter 'feGroup'.
+* QFQ Typo3 Updatewizard: Sort output by page and tt-content ordering. Add page uid an title to be reported.
+  Inline Edit: add tt_content uid and header as tooltip.
+* Update package.json.
+* Update to PHPUnit version 9.
+
+Bug Fixes
+^^^^^^^^^
+
+* #12468 / Form: Dynamic Update Form.title after save.
+* #14636 / 'Clear Me'-cross lighter and 2 px up.
+* #15445 / Doc: JS Output Widged added.
+* #15497 / Delete link: a) broken for 'table', b) broken with 'r:3'.
+* #15527 / Form/subrecord: class 'qfq-table-50' no impact.
+* #15654 / Form FE required 'Undefined index Error'.
+* #15659 / Multi Form: Datetime Picker not aligned.
+* #15691 / T3 V10: Export PDF (PDF Generator) invalid link causes error.
+* #15726 / Formelement upload stays required when changed from required to hidden.
+* #15729 / Form: text readonly input should show newline as '<br>'.
+* #15747 / Typeahead does not work with special character. Added croatian special chars (Ć,ć,Č,č,Đ,đ,Š,š,Ž,ž) to alnumx
+  whitelist.
+* #15763 / Search/Refactor: a) search for ID, b) message if nothing is found - New highlighting for disabled content.
+  Added output for not found in search.
+* #15773 / TypeAhead missing check type warning.
+* #15813 / Pressing 'Enter' in Form asks: Do you really want to delete the record?
+* #15875 / Unecessary qfq-config save.
+* #15905 / FE type time undefined index.
+* #15913 / Refactor: Footer displayed at the wrong place.
+* #15921 / Frontend edit Report Codemirror resizeable.
+* #16004 / Template Group Fields broken.
+* #16046 / Datepicker not initialized in Template Group.
+* #16051 / Dropdown menu: Download file broken with ZIP File.
+* #16055 / Dropdown option with render mode 3 format problem.
+* #16064 / Dynamic Update Checkbox and Form Store in parameter.
+* #16073 / saveButtonActive is disabled after first save.
+* #16201 / Character Count Class Quotation Mark.
+* #16204 / TypeAhead Prefetch Expects String Array Given.
+* #16228 / extraButtonPassword unhide broken.
+* #16264 / Multi DB Broken since QFQ V23.2.0.
+* #16273 / TinyMCE image upload path incorrect for T3 v10 and upwards.
+
+Version 23.3.1
+--------------
+
+Date: 31.03.2023
+
+Bug Fixes
+^^^^^^^^^
+
+* #15920 / QFQ variable evaluation broken: wrong variable name
+
+Version 23.3.0
+--------------
+
+Date: 30.03.2023
+
+Features
+^^^^^^^^
+
+* #15491 / Search refactor redesign and T3 V10 compatiblity: underscore character, counter fixed, page alias and slug handling.
+* #15529 / Form/subrecord: Design Titel / Box.
+* #15570 / Changed DB handling in class FormAction. Fixed multi-db problem with this. Included change for check of
+  existing form editor report.
+* #15579 / Ability for searching all possible characters, includes not replaced QFQ variables.
+* #15627 / Added character count and maxLength feature for TinyMCE.
+* Add various icons to documentation.
+* Doc Form.rst: reference 'orderColumn'.
+* Doc Report.rst: fix typo, add icons, improved example for tablesorter.
+* Add indexes for table FormSubmitLog.
+* FormEditor: Show Tablename in pill 'table definition'.
+* FormEditor: FE subrecord > show container id below name.
+
+Bug Fixes
+^^^^^^^^^
+
+* #14754 / Using double quotes in tableview config caused sql error.
+* #15483 / Protected folder check fixed. Changed default of wget, preventing errors. Changed handling from protected
+  folder check, new once a day.
+* #15521 / FormEditor assigns always container, even none is selected. Change handling of form variables from type select,
+  radio and checkbox. Expected 0 from client request instead of empty string.
+* #15523 / Search/Refactor broken for Multi-DB.
+* #15626 / Multi-DB: FormEditor save error.
+
 Version 23.2.0
 --------------
 
diff --git a/Documentation/Report.rst b/Documentation/Report.rst
index 0c39a29a467f56bac57ca31b0c5a9f8082f31497..0319915e9f60ef32a331869c8270dc8d7e32c263 100644
--- a/Documentation/Report.rst
+++ b/Documentation/Report.rst
@@ -398,8 +398,8 @@ To get the same result, the following is also possible::
                                     '|p:/export',
                                     '|t:Download') AS _pdf
 
-Nesting of levels
-^^^^^^^^^^^^^^^^^
+Nesting of levels: `numeric`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Levels can be nested. E.g.::
 
@@ -417,6 +417,58 @@ This is equal to::
     10.5.sql = SELECT ...
     10.5.head = ...
 
+Nesting of levels: `alias`
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Levels can be nested without levels. E.g.::
+
+  {
+    sql = SELECT ...
+    {
+        sql = SELECT ...
+        head = ...
+    }
+  }
+
+This is equal to::
+
+  1.sql = SELECT ...
+  1.2.sql = SELECT ...
+  1.2.head = ...
+
+Levels are automatically numbered from top to bottom.
+
+An alias can be used instead of levels. E.g.::
+
+  myAlias {
+    sql = SELECT ...
+    nextAlias {
+        sql = SELECT ...
+        head = ...
+    }
+  }
+
+This is also equal to::
+
+  1.sql = SELECT ...
+  1.2.sql = SELECT ...
+  1.2.head = ...
+
+.. important::
+
+Allowed characters for an alias: [a-zA-Z0-9_-].
+
+.. important::
+
+The first level determines whether report notation `numeric` or `alias` is used. Using an alias or no level triggers
+report notation `alias`. It requires the use of delimiters throughout the report. A combination with the notation
+'10.sql = ...' is not possible.
+
+.. important::
+
+Report notation `alias` does not require that each level be assigned an alias. If an alias is used, it must be on the
+same line as the opening delimiter.
+
 Leading / trailing spaces
 ^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -532,7 +584,20 @@ Example 'level'::
     10.5.20.sql = SELECT '{{10.pId}}'
     10.10.sql = SELECT '{{10.pId}}'
 
+Example 'alias'::
 
+  myAlias {
+    sql = SELECT p.id AS _pId, p.name FROM Person AS p
+    myAlias2 {
+        sql = SELECT adr.city, 'dummy' AS _pId FROM Address AS adr WHERE adr.pId={{10.pId}}
+        myAlias3 {
+            sql = SELECT '{{myAlias.pId}}'
+        }
+    }
+    myAlias4 {
+        sql = SELECT '{{myAlias.pId}}'
+    }
+  }
 Notes to the level:
 
 +-------------+------------------------------------------------------------------------------------------------------------------------+
@@ -546,6 +611,8 @@ Notes to the level:
 +-------------+------------------------------------------------------------------------------------------------------------------------+
 | Child       |The level *30* has one child and child child: *30.5* and *30.5.1*                                                       |
 +-------------+------------------------------------------------------------------------------------------------------------------------+
+| Alias       |A variable that can be assigned to a level and used to retrieve its values.                                             |
++-------------+------------------------------------------------------------------------------------------------------------------------+
 | Example     | *10*, *20*, *30*, *50** are root level and will be completely processed one after each other.                          |
 |             | *30.5* will be executed as many times as *30* has row numbers.                                                         |
 |             | *30.5.1*  will be executed as many times as *30.5* has row numbers.                                                    |
@@ -712,6 +779,8 @@ Summary:
 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 |_decrypt                |:ref:`column-decrypt` - Decrypt value.                                                                                                                                                       |
 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|_jwt                    |:ref:`column-jwt` - generates a json web token from the provided data.                                                                                                                       |
++------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 
 
 .. _column-link:
@@ -1802,6 +1871,38 @@ Decrypting selected columns or strings which are encrypted with QFQ.
 
     10.sql = SELECT secret AS _decrypt FROM Person WHERE id = 1
 
+.. _column-jwt:
+
+Column: _jwt
+^^^^^^^^^^^^
+
+Creates a `json web token <https://jwt.io/>`_ from the provided data.
+
+Supported options:
+
++-----------+---------------+------------------------------------------------------+
+| Parameter | Default value | Note                                                 |
++===========+===============+======================================================+
+| `alg`     | `HS256`       | The signing algorithm - it is included in the header |
+| `key`     | (none)        | The secret key used for signing the token            |
++-----------+---------------+------------------------------------------------------+
+
+Predefined claims:
+
++-------+----------------+-------------------+-----------------------------------------------------------+
+| Claim | Present        | Default value     | Note                                                      |
++=======+================+===================+===========================================================+
+| `iss` | always         | `qfq`             | The default value might be also specified in QFQ settings |
+| `iat` | always         | current timestamp | Ignores any provided value                                |
+| `exp` | when specified | none              | Prefix with `+` to specify a relative timestamp           |
+| `nbf` | when specified | none              | Prefix with `+` to specify a relative timestamp           |
++-------+----------------+-------------------+-----------------------------------------------------------+
+
+**Syntax** ::
+
+    10.sql = SELECT 'exp:+3600,data:{msg:default alg}|<secretKey>' AS _jwt
+    20.sql = SELECT 'exp:+60,data:{msg:explicit agl}|<secretKey>|ES384' AS _jwt
+
 .. _copyToClipboard:
 
 Copy to clipboard
diff --git a/Documentation/Variable.rst b/Documentation/Variable.rst
index 12b6552e24d8e900a60e6e21bf92f154130c028f..423adc417caa87f3d9ed69cced48d99ea7635824 100644
--- a/Documentation/Variable.rst
+++ b/Documentation/Variable.rst
@@ -410,11 +410,11 @@ Example::
 Row column variables
 --------------------
 
-Syntax:  *{{<level>.<column>}}*
+Syntax:  *{{<level>.<column>}}* or *{{<alias>.<column>}}*
 
 Only used in report to access outer columns. See :ref:`access-column-values` and :ref:`syntax-of-report`.
 
-There might be name conflicts between VarName / SQL keywords and <line identifier>. QFQ checks first for *<level>*,
+There might be name conflicts between VarName / SQL keywords and <line identifier>. QFQ checks first for *<level>* and *<alias>*,
 than for *SQL keywords* and than for *VarNames* in stores.
 
 All types might be nested with each other. There is no limit of nesting variables.
diff --git a/Gruntfile.js b/Gruntfile.js
index d65c613472645ab489b08678db00c5a4c8234683..6398f40238b95fb4343fd54b67131951088e6003 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -587,9 +587,9 @@ module.exports = function (grunt) {
                 ]
             },
         },
-        uglify: {
+        terser: {
             options: {
-                banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
+                //banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
             },
             build: {
                 src: ['js/<%= pkg.name %>.debug.js'],
@@ -613,7 +613,10 @@ module.exports = function (grunt) {
             }
         },
         jshint: {
-            all: js_sources
+            all: js_sources,
+            options: {
+                'esversion': 6,
+            }
         },
         concat_in_order: {
             debug_standalone: {
@@ -714,7 +717,8 @@ module.exports = function (grunt) {
     });
 
     // Load the plugin that provides the "uglify" task.
-    grunt.loadNpmTasks('grunt-contrib-uglify');
+    grunt.loadNpmTasks('grunt-terser');
+    //grunt.loadNpmTasks('grunt-contrib-uglify');
     grunt.loadNpmTasks('grunt-contrib-copy');
     grunt.loadNpmTasks('grunt-contrib-concat');
     grunt.loadNpmTasks('grunt-concat-in-order');
@@ -724,9 +728,9 @@ module.exports = function (grunt) {
     grunt.loadNpmTasks('grunt-contrib-jasmine');
 
     // Default task(s).
-    grunt.registerTask('default', ['jshint', 'concat_in_order', 'uglify', 'copy', 'less']);
+    grunt.registerTask('default', ['jshint', 'concat_in_order', 'terser', 'copy', 'less']);
 
-    grunt.registerTask('only-js', ['jshint', 'concat_in_order', 'uglify', 'copy:qfqPlugins', 'copy:worker']);
+    grunt.registerTask('only-js', ['jshint', 'concat_in_order', 'terser', 'copy:qfqPlugins', 'copy:worker']);
 
     grunt.registerTask('run-jasmine', ['jshint', 'concat_in_order', 'jasmine']);
 
diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php
index b1749c67af12af276e473734bff159ac02c3cee4..50c069825f3b5447e812a1ad5ef6b057abb6166a 100644
--- a/extension/Classes/Core/AbstractBuildForm.php
+++ b/extension/Classes/Core/AbstractBuildForm.php
@@ -640,7 +640,9 @@ abstract class AbstractBuildForm {
      */
     public function getFormId() {
         if ($this->formId === null) {
-            $this->formId = uniqid('qfq-form-');
+//            $this->formId = uniqid('qfq-form-');
+            $this->formId = 'qfq-form-' . $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3) . '-'
+                . $this->formSpec[F_ID] . '-' . $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_SIP . STORE_RECORD . STORE_ZERO);
         }
 
         return $this->formId;
@@ -904,7 +906,7 @@ abstract class AbstractBuildForm {
             // Typically: $htmlElementNameIdZero = true
             // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
             $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
-            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
+            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->getFormId(), $formElement[FE_ID],
                 ($htmlElementNameIdZero) ? 0 : $recordId,
                 $formElement[FE_TG_INDEX]);
 
diff --git a/extension/Classes/Core/BodytextParser.php b/extension/Classes/Core/BodytextParser.php
index 2b5394e383fdd34607fbd27b20bd20f5e90b30ef..9e4fd8d50caa681e325ec0b27eb42043dc5dcea0 100644
--- a/extension/Classes/Core/BodytextParser.php
+++ b/extension/Classes/Core/BodytextParser.php
@@ -49,6 +49,8 @@ class BodytextParser {
                 json_encode([ERROR_MESSAGE_TO_USER => 'Report: Missing close delimiter', ERROR_MESSAGE_TO_DEVELOPER => $bodyText]), ERROR_MISSING_CLOSE_DELIMITER);
         }
 
+        unset($this->reportLinesTemp);
+
         return $bodyText;
     }
 
@@ -64,6 +66,7 @@ class BodytextParser {
 
     private function trimAndRemoveCommentAndEmptyLine($bodytext, &$nestingOpen, &$nestingClose) {
         $data = array();
+        $reportLines = array();
 
         $src = explode(PHP_EOL, $bodytext);
         if ($src === false) {
@@ -72,22 +75,27 @@ class BodytextParser {
 
         $firstLine = trim($src[0]);
 
-        foreach ($src as $row) {
+        foreach ($src as $key => $row) {
             $row = trim($row);
 
             if ($row === '' || $row[0] === '#') {
                 continue;
             }
             $data[] = $row;
+
+            // Increment $key to match line from tt-content record
+            $key++;
+            $reportLines[] = $key;
         }
 
+        $this->reportLinesTemp = $reportLines;
         $this->setNestingToken($firstLine, $nestingOpen, $nestingClose);
 
         return implode(PHP_EOL, $data);
     }
 
     /**
-     * Set the 'nesting token for this tt-conten record. Valid tokens are {}, <>, [], ().
+     * Set the nesting token for this tt-content record. Valid tokens are {}, <>, [], ().
      * If the first line of bodytext is a comment line and the last char of that line is a valid token: set that one.
      * If not: set {} as nesting token.
      *
@@ -170,7 +178,9 @@ class BodytextParser {
      */
     private function joinLine($bodyText, $nestingOpen, $nestingClose) {
         $data = array();
+        $reportLines = $this->reportLinesTemp;
         $bodytextArray = explode(PHP_EOL, $bodyText);
+        $firstToken = '';
 
         $nestingOpenRegexp = $nestingOpen;
         if ($nestingOpen === '(' || $nestingOpen === '[') {
@@ -179,7 +189,7 @@ class BodytextParser {
 
         $full = '';
         $joinDelimiter = ' ';
-        foreach ($bodytextArray as $row) {
+        foreach ($bodytextArray as $key => $row) {
 
             // Line end with '\'?
             if (substr($row, -1) == '\\') {
@@ -192,8 +202,11 @@ class BodytextParser {
             if (($row == $nestingOpen || $row == $nestingClose)
                 || (1 === preg_match('/^\d+(\.\d+)*(\s*' . $nestingOpenRegexp . ')?$/', $row))
                 || (1 === preg_match('/^(\d+\.)*(' . TOKEN_VALID_LIST . ')\s*=/', $row))
-            ) {
 
+                // Report notation 'alias'
+                // E.g. myAlias { ...
+                || (1 === preg_match('/^[\w-]*(\s*' . $nestingOpenRegexp . ')+$/', $row))
+            ) {
                 // if there is already something: save this.
                 if ($full !== '') {
                     $data[] = $full;
@@ -202,9 +215,26 @@ class BodytextParser {
                 // start new line
                 $full = $row;
 
+                // This later determines the notation mode
+                // Possible values for $firstToken: '10', '10.20', '10.sql=...', '10.head=...', 'myAlias {', 'myAlias{'
+                // Values such as 'form={{form:SE}}' are valid but not parsed as a level/alias.
+                if (empty($firstToken) && 1 !== preg_match('/^(' . TOKEN_VALID_LIST . ')\s*=/', $row)) {
+                    $firstToken = (strpos($row, $nestingOpen) !== false) ? trim(substr($row, 0, strpos($row, $nestingOpen))) : $row;
+                }
+
+                // If the open delimiter is missing while using an alias, this is necessary to get the correct error message later on
+                // Starts a new line if the previous line only contained '}'
+                // It prevents that the lines '}' and 'myAlias' will be joined
+            } elseif ($full === $nestingClose) {
+                $data[] = $full;
+
+                // start new line
+                $full = $row;
             } else {
                 // continue row: concat - the space is necessary to join SQL statements correctly: 'SELECT ... FROM ... WHERE ... AND\np.id=...'  - here a 'AND' and 'p.id' need a space.
                 $full .= $joinDelimiter . $row;
+                // remove unused elements
+                unset($reportLines[$key]);
             }
 
             $joinDelimiter = $joinDelimiterNext;
@@ -215,6 +245,29 @@ class BodytextParser {
             $data[] = $full;
         }
 
+        $reportLines = array_values($reportLines);
+
+        // Combines line numbers ($key) from tt-content record with content ($value) from corresponding line: [line => content]
+        // E.g. [0 => 4, 1 => "[", 2 => "sql = SELECT ...", 3 => "[", 4 => "sql = SELECT ...", ...]: line 0 is empty
+        foreach ($reportLines as $key => $value) {
+            $reportLines[$value] = $data[$key];
+        }
+
+        // Removes every element that is not an SQL statement: [line => content]
+        // E.g. [2 => "sql = SELECT ...", 4 => "sql = SELECT ...", ...]
+        foreach ($reportLines as $key => $value) {
+            if (strpos($value, '=') !== false) {
+                $arr = explode('"', $value, 2);
+                if (strpos($arr[0], TOKEN_SQL) === false) {
+                    unset($reportLines[$key]);
+                }
+            } else {
+                unset($reportLines[$key]);
+            }
+        }
+        $this->reportLinesTemp = $reportLines;
+        $this->firstToken = $firstToken;
+
         return implode(PHP_EOL, $data);
     }
 
@@ -237,7 +290,14 @@ class BodytextParser {
             $nestingClose = '\\' . $nestingClose;
         }
 
-        $bodytext = preg_replace('/^((\d+)(\.\d+)*\s*)?(' . $nestingOpen . ')$/m', '$1' . NESTING_TOKEN_OPEN, $bodytext);
+        // Report notation 'numeric'
+        // E.g. 10 { ...
+        // $bodytext = preg_replace('/^((\d+)(\.\d+)*\s*)?(' . $nestingOpen . ')$/m', '$1' . NESTING_TOKEN_OPEN, $bodytext);
+
+        // Report notation 'alias'
+        // E.g. myAlias { ...
+        $bodytext = preg_replace('/^((\s*[\w-]*\s*)|((\s*\d+)(\.\d+)*\s*))?(' . $nestingOpen . ')/m', '$1' . NESTING_TOKEN_OPEN, $bodytext);
+
         $bodytext = preg_replace('/^' . $nestingClose . '$/m', '$1' . NESTING_TOKEN_CLOSE, $bodytext);
 
         return $bodytext;
@@ -277,6 +337,28 @@ class BodytextParser {
         $result = $bodytext;
         $posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
 
+        // Default: Report notation 'numeric'
+        $notationMode = TOKEN_NOTATION_NUMERIC;
+        $levels = null;
+        $reportLines = $this->reportLinesTemp;
+        $alias = null;
+        $aliases = null;
+        $firstToken = $this->firstToken;
+
+        // No first token or non-numeric first token implies report notation 'alias'
+        // It supports auto numbering of blocks and aliases
+        if (empty($firstToken) || (1 !== preg_match('/^([0-9\._-])+$/', $firstToken) && !strpos($firstToken, '='))) {
+            $notationMode = TOKEN_NOTATION_ALIAS;
+            $aliases = array();
+            $levels = array();
+            $maxLevel = substr_count($bodytext, NESTING_TOKEN_CLOSE);
+
+            // Generate an array containing all levels, e.g. [1,2,3,...]
+            for ($x = 1; $x <= $maxLevel; $x++) {
+                array_push($levels, $x);
+            }
+        }
+
         while ($posFirstClose !== false) {
             $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN);
 
@@ -285,7 +367,6 @@ class BodytextParser {
                 throw new \UserFormException(
                     json_encode([ERROR_MESSAGE_TO_USER => 'Missing open delimiter', ERROR_MESSAGE_TO_DEVELOPER => "Missing open delimiter: $result"]),
                     ERROR_MISSING_OPEN_DELIMITER);
-
             }
 
             $pre = substr($result, 0, $posMatchOpen);
@@ -304,11 +385,57 @@ class BodytextParser {
             $levelStartPos = ($levelStartPos === false) ? 0 : $levelStartPos + 1;  // Skip PHP_EOL
 
             $level = trim(substr($pre, $levelStartPos));
-//            if($level==='') {
-//                $pre=
-//            }
-            // remove 'level' from last line
-            $pre = substr($pre, 0, $levelStartPos);
+
+            // Report notation 'alias'
+            // Count open brackets in front of current level
+            // E.g. current $level = 2, then $index = 1, because there is 1 in front
+            $index = substr_count($pre, NESTING_TOKEN_OPEN);
+
+            // Check for report notation 'alias'
+            if ($notationMode === TOKEN_NOTATION_ALIAS) {
+                $aliasLevel = null;
+
+                // $firstToken === $level checks if we are in the 'first loop'
+                // $adjustLength is used later while extracting a substring and has to be zero in the first loop
+                $adjustLength = ($firstToken === $level && strpos($pre, PHP_EOL) === false) ? 0 : 1;
+
+                // If the $level, from which the $alias is extracted, nothing gets saved
+                // Allowed characters: [a-zA-Z0-9\._-]
+                // '.' is only allowed to detect report notation 'numeric'. This throws an error later on.
+                $alias = (1 === preg_match('/^[a-zA-Z0-9\._-]+$/', $level) || $level === '') ? $level : null;
+
+                // If no alias is set, then nothing gets saved
+                if (!empty($alias)) {
+                    // Construct absolute $level of the current $alias
+                    // E.g. 1.2.3.
+                    for ($x = 0; $x <= $index; $x++) {
+                        $aliasLevel .= (isset($levels[$x])) ? $levels[$x] . '.' : null;
+                    }
+
+                    // Trailing '.' gets removed from $level: E.g. 1.2.3
+                    // $level is saved together with $alias: [level => alias]
+                    $aliases[substr($aliasLevel, 0, strlen($aliasLevel) - 1)] = $alias;
+                }
+
+                // Current $level can now be extracted from $levels [1,2,3,...]
+                $level = (isset($levels[$index])) ? $levels[$index] : null;
+
+                // Remove current $level from $levels [1,3,...]
+                // This works because opening brackets get removed from $pre after every level
+                unset($levels[$index]);
+
+                // Reset keys
+                $levels = array_values($levels);
+
+                // Removes alias or level added by user to continue auto numbering scheme
+                // E.g. User input: {\nsql = SELECT ...\n}\nmyAlias{\nsql = SELECT ...\n}
+                // $pre = "1.sql = SELECT ...\nmyAlias" -> $pre = "1.sql = SELECT ...\n"
+                $pre = substr($pre, 0, strrpos($pre, PHP_EOL) + $adjustLength);
+            } else {
+
+                // Remove 'level' from last line
+                $pre = substr($pre, 0, $levelStartPos);
+            }
 
             // Split nested content in single rows
             $lines = explode(PHP_EOL, $match);
@@ -326,6 +453,33 @@ class BodytextParser {
 //        $result = str_replace('#&]_#', '}', $result);
 //        $result = Support::decryptDoubleCurlyBraces($result);
 
+        $resultArr = explode(PHP_EOL, $result);
+
+        // $value (previously SQL statement) gets replaced by its level: [line => level]
+        // E.g. [2 => 1, 4 => 1.2, ...]:
+        foreach ($reportLines as $keyLines => $valueLines) {
+            foreach ($resultArr as $keyResult => $valueResult) {
+                if (strpos($valueResult, '=')) {
+                    $arr = explode("=", $valueResult, 2);
+                    if (strpos($arr[0], TOKEN_SQL) !== false) {
+                        $reportLines[$keyLines] = str_replace('.' . TOKEN_SQL, '', trim($arr[0]));
+                    } else {
+                        continue;
+                    }
+                } else {
+                    continue;
+                }
+                unset($resultArr[$keyResult]);
+                break;
+            }
+        }
+
+        // Array is flipped: [level => line]
+        // E.g. [1 => 2, 1.2 => 4, ...]
+        $this->reportLines = array_flip($reportLines);
+
+        $this->aliases = $aliases;
+
         return $result;
     }
 
diff --git a/extension/Classes/Core/BuildFormBootstrap.php b/extension/Classes/Core/BuildFormBootstrap.php
index a15737c31026c42fe1201f254774bd3f2625c6df..75e81f72e6b11c04d976c8651c1cee51cd916edf 100644
--- a/extension/Classes/Core/BuildFormBootstrap.php
+++ b/extension/Classes/Core/BuildFormBootstrap.php
@@ -448,8 +448,9 @@ class BuildFormBootstrap extends AbstractBuildForm {
         $class = Support::doAttribute('class', $class);
         $dataClassOnChange = Support::doAttribute('data-class-on-change', $buttonOnChangeClass);
         $tooltip = Support::doAttribute('title', $tooltip);
+        $formId = $this->getFormId();
 
-        return "<button id='$buttonHtmlId' type='button' $class $dataClassOnChange $tooltip $disabled>$element</button>";
+        return "<button id='$buttonHtmlId-$formId' type='button' $class $dataClassOnChange $tooltip $disabled>$element</button>";
     }
 
     /**
@@ -510,7 +511,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
 //            $a = '<a ' . Support::doAttribute('href', '#' . $this->createAnker($formElement[FE_ID])) . ' data-toggle="tab">' . $formElement[FE_LABEL] . '</a>';
 
             $attributeLiA = 'data-toggle="tab" ';
-            $hrefTarget = '#' . $this->createAnker($formElement[FE_ID]);
+            $hrefTarget = '#' . $this->createAnker($formElement[FE_ID], $recordId);
 
             $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, $recordId);
             switch ($formElement[FE_MODE]) {
@@ -582,15 +583,16 @@ class BuildFormBootstrap extends AbstractBuildForm {
      *
      * @return string
      */
-    private function createAnker($id) {
-        return $this->formSpec[FE_NAME] . '_' . $id;
+    private function createAnker($id, $recordId) {
+        return $this->formSpec[FE_NAME] . '_' . $id . '_' . $recordId;
     }
 
     /**
      * @return string
      */
     private function getTabId() {
-        return 'qfqTabs';
+        return 'qfqTabs-' . $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3) . '-'
+        . $this->formSpec[F_ID] . '-' . $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_SIP . STORE_RECORD . STORE_ZERO);
     }
 
     /**
@@ -606,7 +608,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
 
         $attribute = $this->getFormTagAttributes();
 
-        $attribute['class'] = 'form-horizontal';
+        $attribute['class'] = 'form-horizontal qfq-form';
         $attribute['data-toggle'] = 'validator';
 
         $formModeGlobal = Support::getFormModeGlobal($this->formSpec[F_MODE_GLOBAL] ?? '');
@@ -622,6 +624,27 @@ class BuildFormBootstrap extends AbstractBuildForm {
         $honeypot = $this->getHoneypotVars();
         $md5 = $this->buildInputRecordHashMd5();
 
+        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
+        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;
+
+        # Replacing the attributes set via tail / javascript
+        $attribute["data-form-id"] = $this->getFormId();
+        $attribute["data-tabs-id"] = $this->getTabId();
+        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
+            $attribute["data-delete-url"] = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
+        }
+        if ($this->formSpec[F_DIRTY_MODE] === DIRTY_MODE_NONE) {
+            $attribute["data-dirty-url"] = Path::urlApi(API_DIRTY_PHP);
+        }
+        $attribute["data-submit-to"] = Path::urlApi(API_SAVE_PHP);
+        $attribute["data-type-ahead-url"] = Path::urlApi(API_TYPEAHEAD_PHP);
+        $attribute["data-refresh-url"] = Path::urlApi(API_LOAD_PHP);
+        $attribute["data-file-upload-to"] = Path::urlApi(API_FILE_PHP) . '?' . $actionUpload;
+        $attribute["data-file-delete-url"] = Path::urlApi(API_FILE_PHP) . '?' . $actionDelete;
+        $attribute["data-log-level"] = 0; # Not sure if raos did implement this fully, but it was hardcoded 0 before.
+        $attribute["data-api-delete-url"] = Path::urlApi(API_DELETE_PHP);
+        #$attribute["data-sip"] = 
+
         return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot . $md5;
     }
 
@@ -634,58 +657,10 @@ class BuildFormBootstrap extends AbstractBuildForm {
     public function tail() {
 
         $html = '';
-        $deleteUrl = '';
-
-        $formId = $this->getFormId();
 
         $html .= '</div> <!--class="tab-content" -->';  //  <div class="tab-content">
 //        $html .= '<input type="submit" value="Submit">';
 
-        $formId = $this->getFormId();
-        $tabId = $this->getTabId();
-
-        if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) {
-            $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], $recordId);
-        }
-
-        $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
-        $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE;
-
-        $apiDeletePhp = Path::urlApi(API_DELETE_PHP);
-
-        $dirtyAction = ($this->formSpec[F_DIRTY_MODE] == DIRTY_MODE_NONE) ? '' : "dirtyUrl: '" . Path::urlApi(API_DIRTY_PHP) . "',";
-
-        $submitTo = Path::urlApi(API_SAVE_PHP);
-        $typeAheadUrl = Path::urlApi(API_TYPEAHEAD_PHP);
-        $refreshUrl = Path::urlApi(API_LOAD_PHP);
-        $fileUploadTo = Path::urlApi(API_FILE_PHP) . '?' . $actionUpload;
-        $fileDeleteUrl = Path::urlApi(API_FILE_PHP) . '?' . $actionDelete;
-
-        $html .= '</form>';  //  <form class="form-horizontal" ...
-        $html .= <<<EOF
-        <script type="text/javascript">
-            $(function () {
-                'use strict';
-                QfqNS.Log.level = 0;
-
-                var qfqPage = new QfqNS.QfqPage({
-                    tabsId: '$tabId',
-                    formId: '$formId',
-                    submitTo: '$submitTo',
-                    $dirtyAction
-                    deleteUrl: '$deleteUrl',
-                    typeAheadUrl: '$typeAheadUrl',
-                    refreshUrl: '$refreshUrl',
-                    fileUploadTo: '$fileUploadTo',
-                    fileDeleteUrl: '$fileDeleteUrl'
-                });
-
-               
-                var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
-            })
-         </script>
-EOF;
-
         // Button Save at bottom of form - only if there is a button text given.
         if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') {
 
@@ -710,9 +685,9 @@ EOF;
             $html .= $this->wrapItem(WRAP_SETUP_INPUT, $htmlElement);
             $html .= $this->wrapItem(WRAP_SETUP_NOTE, '');
             $html .= $lastDiv;
-
         }
 
+        $html .= '</form>';
 
         $html .= '</div>';  //  <div class="container-fluid"> === main <div class=...> around everything
 
@@ -892,8 +867,9 @@ EOF;
             $class = $this->isFirstPill ? 'active ' : '';
             $this->isFirstPill = false;
         }
+        $recordId = $this->store->getVar(COLUMN_ID, STORE_RECORD . STORE_ZERO);
 
-        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id']) . '">', $html);
+        $html = Support::wrapTag('<div role="tabpanel" class="tab-pane ' . $class . '" id="' . $this->createAnker($formElement['id'], $recordId) . '">', $html);
 
 
         return $html;
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index 595150708ae3b7a1df34c8d0e40ce30eec2eed67..6eec6c4668602c443115434f2b0ad53500188690 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -257,6 +257,7 @@ const ERROR_INVALID_SAVE_PDF_FILENAME = 1410;
 const ERROR_TWIG_COLUMN_NOT_UNIQUE = 1411;
 const ERROR_DOUBLE_DEFINITION = 1412;
 const ERROR_INVALID_SAVE_ZIP_FILENAME = 1413;
+const ERROR_NUMERIC_ALIAS = 1414;
 
 // Upload
 const ERROR_UPLOAD = 1500;
@@ -526,6 +527,7 @@ const TYPO3_PAGE_DESCRIPTION = 'pageDescription';
 const TYPO3_PAGE_KEYWORDS = 'pageKeywords';
 const TYPO3_PAGE_NAV_TITLE = 'pageNavTitle';
 const TYPO3_VERSION = 't3Version';
+const TYPO3_TOKEN_REPORT_LINE = 'parsed';
 
 const TYPO3_PAGE_LANGUAGE = SESSION_PAGE_LANGUAGE;
 const TYPO3_PAGE_LANGUAGE_PATH = SESSION_PAGE_LANGUAGE_PATH;
@@ -772,6 +774,7 @@ const SYSTEM_REPORT_COLUMN_VALUE = 'reportColumnValue'; // Value of SQL-column p
 const SYSTEM_REPORT_FULL_LEVEL = 'reportFullLevel'; // Full level of current report row. E.g.: 10.20.1. Used for error reports.
 const SYSTEM_MESSAGE_DEBUG = 'messageDebug';
 const SYSTEM_DOWNLOAD_POPUP = 'hasDownloadPopup'; // Marker which is set to 'true' if there is at least one Download Link rendered
+const SYSTEM_REPORT_LINE = 'reportLine';
 const DOWNLOAD_POPUP_REQUEST = 'true';
 const DOWNLOAD_POPUP_REPLACE_TEXT = '#downloadPopupReplaceText#';
 const DOWNLOAD_POPUP_REPLACE_TITLE = '#downloadPopupReplaceTitle#';
@@ -1677,7 +1680,7 @@ const COLUMN_UID = 'uid';
 const COLUMN_HEADER = 'header';
 const COLUMN_TITLE = 'title';
 const COLUMN_SUBHEADER = 'subheader';
-
+const COLUMN_EXPIRE = 'expire';
 const INDEX_PHP = 'index.php';
 
 // QuickFormQuery.php
@@ -1784,6 +1787,9 @@ const TOKEN_DB_INDEX = F_DB_INDEX;
 const TOKEN_DB_INDEX_LC = 'dbindex';
 const TOKEN_CONTENT = 'content';
 const TOKEN_REPORT_FILE = 'file';
+const TOKEN_ALIAS = 'alias';
+const TOKEN_NOTATION_NUMERIC = 'numeric';
+const TOKEN_NOTATION_ALIAS = 'alias';
 
 const TOKEN_VALID_LIST = 'sql|function|twig|head|althead|altsql|tail|shead|stail|rbeg|rend|renr|rsep|fbeg|fend|fsep|fskipwrap|rbgd|debug|form|r|debugShowBodyText|dbIndex|sqlLog|sqlLogMode|content|render';
 
@@ -1871,6 +1877,8 @@ const COLUMN_STORE_USER = '=';
 
 const COLUMN_FORM_JSON = 'formJson';
 
+const COLUMN_JWT = 'jwt';
+
 // Author: Enis Nuredini
 const COLUMN_ENCRYPT = 'encrypt';
 const COLUMN_DECRYPT = 'decrypt';
@@ -2212,6 +2220,7 @@ const EXCEPTION_REPORT_COLUMN_INDEX = 'Report column index'; // Keyname of SQL-c
 const EXCEPTION_REPORT_COLUMN_NAME = 'Report column name'; // Keyname of SQL-column processed at the moment.
 const EXCEPTION_REPORT_COLUMN_VALUE = 'Report column value'; // Keyname of SQL-column processed at the moment.
 const EXCEPTION_REPORT_FULL_LEVEL = 'Report level key';
+const EXCEPTION_REPORT_LINE = 'Report line';
 
 const EXCEPTION_SIP = 'current sip';
 const EXCEPTION_PAGE_ID = 'Page Id';
diff --git a/extension/Classes/Core/Exception/DbException.php b/extension/Classes/Core/Exception/DbException.php
index 9ee26dec9f50df5b6515a4db06084356f6fe9d63..4eb1d7b8154b1631faada0d0ef4286a04af27fff 100644
--- a/extension/Classes/Core/Exception/DbException.php
+++ b/extension/Classes/Core/Exception/DbException.php
@@ -61,6 +61,7 @@ class DbException extends AbstractException {
         $this->messageArrayDebug[EXCEPTION_SQL_FINAL] = Store::getVar(SYSTEM_SQL_FINAL, STORE_SYSTEM);
         $this->messageArrayDebug[EXCEPTION_SQL_PARAM_ARRAY] = Store::getVar(SYSTEM_SQL_PARAM_ARRAY, STORE_SYSTEM);
         $this->messageArrayDebug[EXCEPTION_REPORT_FULL_LEVEL] = Store::getVar(SYSTEM_REPORT_FULL_LEVEL, STORE_SYSTEM);
+        $this->messageArrayDebug[EXCEPTION_REPORT_LINE] = Store::getVar(SYSTEM_REPORT_LINE, STORE_SYSTEM);
 
         return parent::formatException();
     }
diff --git a/extension/Classes/Core/Form/Dirty.php b/extension/Classes/Core/Form/Dirty.php
index 457190674aee58a4df82eb765fe74724fee7d4da..6c4c42dc6f0eaeac9720e34da74dda66873ea60e 100644
--- a/extension/Classes/Core/Form/Dirty.php
+++ b/extension/Classes/Core/Form/Dirty.php
@@ -10,6 +10,7 @@ namespace IMATHUZH\Qfq\Core\Form;
 
 use IMATHUZH\Qfq\Core\Database\Database;
 use IMATHUZH\Qfq\Core\Helper\OnArray;
+use IMATHUZH\Qfq\Core\Helper\Support;
 use IMATHUZH\Qfq\Core\Store\Client;
 use IMATHUZH\Qfq\Core\Store\Session;
 use IMATHUZH\Qfq\Core\Store\Sip;
@@ -67,7 +68,7 @@ class Dirty {
             $this->client[DIRTY_RECORD_HASH_MD5] = '';
         }
         $this->doDbArray($dbIndexData, $dbIndexQfq);
-
+        $this->store = Store::getInstance();
     }
 
     /**
@@ -119,7 +120,6 @@ class Dirty {
             return [API_STATUS => 'success', API_MESSAGE => ''];
         }
 
-        $this->store = Store::getInstance();
         $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
 
         $this->dbIndexData = empty($sipVars[PARAM_DB_INDEX_DATA]) ? $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM) : $sipVars[PARAM_DB_INDEX_DATA];
@@ -180,13 +180,15 @@ class Dirty {
             if ($formDirtyMode == DIRTY_MODE_NONE) {
                 $answer = [API_STATUS => 'success', API_MESSAGE => ''];
             } else {
-                // No dirty record found.
+                // No dirty record found: create lock
                 $answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5, $tabUniqId);
             }
         } else {
             if ($tabUniqId == $recordDirty[TAB_UNIQ_ID]) {
+                // In case it's the same tab (page reload): OK
                 $answer = [API_STATUS => 'success', API_MESSAGE => ''];
             } else {
+                // Here is probably a conflict.
                 $answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey);
             }
         }
@@ -221,6 +223,7 @@ class Dirty {
     }
 
     /**
+     * Aquire lock conflict detected
      *
      * @param array $recordDirty
      * @param string $currentFormDirtyMode
@@ -233,17 +236,20 @@ class Dirty {
      */
     private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) {
         $status = API_ANSWER_STATUS_CONFLICT;
-        $at = "at " . $recordDirty[COLUMN_CREATED] . " from " . $recordDirty[DIRTY_REMOTE_ADDRESS];
+        $until = "until " . date_format(date_create($recordDirty[COLUMN_EXPIRE]), "d.m.Y H:i:s");
 
-        // Compare modified timestamp
+        // Compare modified timestamp: in case there is a lock conflict and current form is based on outdated data: force reload.
         if ($this->isRecordModified($recordDirty[DIRTY_TABLE_NAME], $primaryKey, $recordDirty[DIRTY_RECORD_ID], $recordDirty[DIRTY_RECORD_HASH_MD5], $dummy)) {
-            return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime. Please reload the form, edit and save again. [2]'];
+            return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime (your changes are lost). Please reload the form, edit and save again.'];
         }
 
-        if ($this->client[CLIENT_COOKIE_QFQ] == $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE]) {
-            $msg = "The record has already been locked by you (maybe in another browser tab) $at!";
-            $status = ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) ? API_ANSWER_STATUS_CONFLICT : API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
+        // Conflict for same user / same QFQ Session: the user can force aquire lock.
+        // Hint: after this, the form with the first lock still thinks it has the lock - that one will get a 'record modified in the meantime' on save.
+        $userMatch = ($recordDirty[DIRTY_FE_USER] != '' && $recordDirty[DIRTY_FE_USER] == $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3));
+        if ($userMatch || $this->client[CLIENT_COOKIE_QFQ] == $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE]) {
 
+            $msg = "Record already locked (by you)";
+            $status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
         } else {
 
             if (empty($recordDirty[DIRTY_FE_USER])) {
@@ -252,7 +258,7 @@ class Dirty {
                 $msgUser = "user '" . $recordDirty[DIRTY_FE_USER] . "'";
             }
 
-            $msg = "The record has already been locked by $msgUser at $at.";
+            $msg = "Record already locked by $msgUser $until.";
 
             // Mandatory lock on Record or current Form?
             if ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE || $currentFormDirtyMode == DIRTY_MODE_EXCLUSIVE) {
@@ -292,6 +298,8 @@ class Dirty {
         # Dirty workaround: setting the 'expired timestamp' minus 1 second guarantees that the client ask for relock always if the timeout is expired.
         $expire = date('Y-m-d H:i:s', strtotime("+" . $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] - 1 . " seconds"));
         // Write 'dirty' record
+
+        $userAgent = $this->store->getVar(CLIENT_HTTP_USER_AGENT, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
         $this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO `Dirty` (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `tabUniqId`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
             "VALUES ( ?,?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
             [$s, $tableName, $recordId, $expire, $recordHashMd5, $tabUniqId, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
@@ -366,20 +374,18 @@ class Dirty {
             return LOCK_NOT_FOUND;
         }
 
-        if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
+        $msgUser = (empty($recordDirty[DIRTY_FE_USER])) ? "another user" : "user '" . $recordDirty[DIRTY_FE_USER] . "'";
+        $rc = LOCK_FOUND_CONFLICT;
+
+        $userMatch = ($recordDirty[DIRTY_FE_USER] != '' && $recordDirty[DIRTY_FE_USER] == $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3));
+        if ($userMatch || $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
             $msgUser = "you";
-        } else {
-            $msgUser = (empty($recordDirty[DIRTY_FE_USER])) ? "another user" : "user '" . $recordDirty[DIRTY_FE_USER] . "'";
+            $rc = LOCK_FOUND_OWNER;
         }
-        $msgAt = "at " . $recordDirty[COLUMN_CREATED] . " from " . $recordDirty[DIRTY_REMOTE_ADDRESS];
-        $msg = "The record has been locked by $msgUser $msgAt";
+        $until = "until " . date_format(date_create($recordDirty[COLUMN_EXPIRE]), "d.m.Y H:i:s");
+        $msg = "The record has been locked by $msgUser $until.";
 
-        // Is the dirtyRecord mine?
-        if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
-            return LOCK_FOUND_OWNER;
-        } else {
-            return LOCK_FOUND_CONFLICT;
-        }
+        return $rc;
     }
 
     /**
diff --git a/extension/Classes/Core/Helper/OnString.php b/extension/Classes/Core/Helper/OnString.php
index 4a22778881710a77760cad737a20bc87086a4bdb..87c099be40a5747bcaa64576a117903e7b74ba97 100644
--- a/extension/Classes/Core/Helper/OnString.php
+++ b/extension/Classes/Core/Helper/OnString.php
@@ -811,7 +811,7 @@ class OnString {
             return array();
         }
 
-        // Search biggest element.
+        // Search the biggest element.
         foreach ($arr as $key => $value) {
 
             if (is_array($value)) {
@@ -858,7 +858,7 @@ class OnString {
                         ERROR_MESSAGE_TO_DEVELOPER => "max: $max, current: " . $currentLength . ", new reduced: " . $newLen]), ERROR_MISSING_OPEN_DELIMITER);
                 } else {
                     // Dive deeper to replace the next biggest element.
-                    $arrNew = $self::limitSizeJsonEncode($arrNew, $newLen, $maxValue);
+                    $arrNew = self::limitSizeJsonEncode($arrNew, $newLen, $maxValue);
                 }
             }
         }
diff --git a/extension/Classes/Core/Helper/Support.php b/extension/Classes/Core/Helper/Support.php
index 3495cd3b8634bcd6ff3baa934aeab8b5711550e8..4dbcbc2f731b6f795ba2eef7f436bb0b12ff699d 100644
--- a/extension/Classes/Core/Helper/Support.php
+++ b/extension/Classes/Core/Helper/Support.php
@@ -152,7 +152,7 @@ class Support {
     public static function arrayToQueryString(array $queryArray) {
         $items = array();
 
-        // CR/Workaround for broken behaviour: LINK() class expects 'id' always as first paramter
+        // CR/Workaround for broken behaviour: LINK() class expects 'id' always as first parameter
         // Take care that Parameter 'id' is the first one in the array:
         if (isset($queryArray[CLIENT_PAGE_ID])) {
             $id = $queryArray[CLIENT_PAGE_ID];
@@ -290,7 +290,7 @@ class Support {
         switch (strtolower($type)) {
             case 'size':
             case 'maxlength':
-                // empty or '0' for attributes of type 'size' or 'maxlength' result in unsuable input elements: skip this.
+                // empty or '0' for attributes of type 'size' or 'maxlength' result in unusable input elements: skip this.
                 if ($value === '' || $value == 0) {
                     return '';
                 }
@@ -378,7 +378,7 @@ class Support {
     }
 
     /**
-     * Search for the parameter $needle in $haystack. The arguments has to be separated by ','.
+     * Search for the parameter $needle in $haystack. The arguments have to be separated by ','.
      *
      * Returns false if not found, or index (starting with 0) of found place. Be careful: use unary operator to compare for 'false'
      *
@@ -630,7 +630,7 @@ class Support {
     }
 
     /**
-     * Returns a representation of 0 in a choosen variant.
+     * Returns a representation of 0 in a chosen variant.
      *
      * @param string $dateFormat FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
      * @param string $showZero
@@ -718,7 +718,7 @@ class Support {
                 $placeholder = $timePattern;
                 break;
             default:
-                throw new \UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE);
+                throw new \UserFormException("Unexpected form element type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE);
         }
 
         return $placeholder;
@@ -726,7 +726,7 @@ class Support {
 
 
     /**
-     * Encrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces.
+     * Encrypt curly braces by an uncommon string. Helps to prevent unwanted actions on curly braces.
      *
      * @param string $text
      *
@@ -740,7 +740,7 @@ class Support {
     }
 
     /**
-     * Decrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces
+     * Decrypt curly braces by an uncommon string. Helps to prevent unwanted actions on curly braces
      *
      * @param string $text
      *
@@ -760,7 +760,7 @@ class Support {
 
     /**
      * Creates a random string, starting with uniq microseconds timestamp.
-     * After a discussion of ME & CR, the uniqid() should be sufficient to guarantee uniqness.
+     * After a discussion of ME & CR, the uniqid() should be sufficient to guarantee uniqueness.
      *
      * @param int $length Length of the required hash string
      *
@@ -782,7 +782,7 @@ class Support {
     }
 
     /**
-     * Concatenate URL and Parameter. Depending of if there is a '?' in URL or not,  append the param with '?' or '&'..
+     * Concatenate URL and Parameter. Depending on if there is a '?' in URL or not,  append the param with '?' or '&'..
      *
      * @param string $url
      * @param string|array $param
@@ -1316,7 +1316,7 @@ class Support {
 
     /**
      * Check $arr, if there is an element $index. If not, set it to $value.
-     * If  $overwriteThis!=false, replace the the original value with $value, if $arr[$index]==$overwriteThis.
+     * If  $overwriteThis!=false, replace the original value with $value, if $arr[$index]==$overwriteThis.
      *
      * @param array $arr
      * @param string $index
diff --git a/extension/Classes/Core/Parser/KVPairListParser.php b/extension/Classes/Core/Parser/KVPairListParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca04529d6b99f4d0842ff6761a1d7de307931712
--- /dev/null
+++ b/extension/Classes/Core/Parser/KVPairListParser.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Class KVPairListParser
+ *
+ * A parser for lists of key-value pairs with simple values.
+ *
+ * @package qfq
+ */
+class KVPairListParser extends SimpleParser {
+
+    /** @var string the separator for different pairs */
+    private string $listsep;
+
+    /** @var string the character separating a key from its value */
+    private string $kvsep;
+
+
+    public function __construct(string $listsep, string $kvsep, array $options = []) {
+        parent::__construct("$listsep$kvsep", $options);
+        $this->listsep = $listsep;
+        $this->kvsep = $kvsep;
+    }
+
+    /**
+     * Iterates over the string and returns keys with values. Keys are always
+     * treated as strings and only values are processed and, when necessary,
+     * converted to numbers or special values.
+     *
+     * Usage:
+     *
+     *   foreach($parser->iterate($input) as $key => $value) {
+     *       ...
+     *   }
+     *
+     * Examples
+     *   pair separator:       |
+     *   key-value separator:  =
+     *
+     *     a=43|b=15         -->   'a'=>43    'b'=>15
+     *     a='x|y' | 13=87   -->   'a'=>'x|y' '13'=>87
+     *
+     * @param string $data
+     * @return \Generator
+     */
+    public function iterate(string $data): \Generator {
+        // Iterate over token provided by the base tokenizer class
+        $tokens = $this->tokenized($data);
+        while ($tokens->valid()) {
+            // Get the key first
+            list($keyToken, $delimiter) = $tokens->current();
+            $key = strval($keyToken);
+            $tokens->next();
+            if ($delimiter == $this->kvsep) {
+                // The key-value separator is found - find the corresponding value
+                if ($tokens->valid()) {
+                    list($valueToken, $delimiter) = $tokens->current();
+                    $tokens->next();
+                    $empty = $valueToken->empty();
+                    $value = $this->process($valueToken);
+                } else {
+                    // The end of the string - the value is empty
+                    $delimiter = null;
+                    $empty = true;
+                }
+                // Replace an empty token with the empty value
+                yield $key => $empty ? (
+                    $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE]
+                ) : $value;
+            } elseif ($key) {
+                // When no key-value separator, then do nothing it the key is empty
+                // In other words: ignore trailing commas
+                yield $key => $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE];
+            }
+            // Check if the current delimiter is a correct one
+            if ($delimiter && $delimiter != $this->listsep) {
+                $this->raiseUnexpectedDelimiter(
+                    $delimiter,
+                    $this->offset(),
+                    $this->listsep
+                );
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/extension/Classes/Core/Parser/MixedTypeParser.php b/extension/Classes/Core/Parser/MixedTypeParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa72870883ae16a9c297f6e85720b1ab83edc131
--- /dev/null
+++ b/extension/Classes/Core/Parser/MixedTypeParser.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+
+/**
+ * Class MixedTypeParser
+ *
+ * A parser for lists and dictionaries that can be nested.
+ * Requires a string of six delimiters provided in this order:
+ * - separator for list items
+ * - key-value separator
+ * - two delimiters (begin and end) for lists
+ * - two delimiters (begin and end) for dictionaries
+ * The parser can be restricted to only nested lists or only
+ * nested dictionaries by providing a space instead of the
+ * corresponding delimiters.
+ *
+ * @package qfq
+ */
+class MixedTypeParser extends SimpleParser {
+
+    // Internally used constants
+    const SEP = 0;
+    const KVSEP = 1;
+    const LIST_START = 2;
+    const LIST_END = 3;
+    const DICT_START = 4;
+    const DICT_END = 5;
+
+    /** @var string delimiters used by this parser */
+    private string $delimiters = ',:[]{}';
+
+
+    public function __construct(?string $delimiters = null, array $options = []) {
+        if ($delimiters) {
+            $this->delimiters = str_pad($delimiters, 6);
+            $delimiters = str_replace(' ', '', $delimiters);
+        } else {
+            $delimiters = $this->delimiters;
+        }
+        parent::__construct($delimiters, $options);
+    }
+
+    /**
+     * Parses the provided string into a literal value, a non-associative array,
+     * or an associative array. The structures can be nested.
+     *
+     * @param string $data
+     * @return mixed
+     */
+    public function parse(string $data) {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            list($data, $empty, $delimiter) = $this->parseImpl($tokens);
+            if ($delimiter) $this->raiseUnexpectedDelimiter($delimiter, $this->offset());
+            return $data;
+        } else {
+            return $this->options[self::OPTION_EMPTY_VALUE];
+        }
+    }
+
+    /**
+     * Assumes the provided string is a list of items and parses
+     * it into a non-associative array (possibly empty or with only
+     * one element).
+     *
+     * @param string $data
+     * @return array
+     */
+    public function parseList(string $data): array {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            return $this->parseListImpl($tokens, null)[0];
+        } else {
+            return [];
+        }
+    }
+
+    /**
+     * Assumes that the provided string is a dictionary (i.e. a list
+     * of key-value pairs) and parses it into an associative array.
+     *
+     * @param string $data
+     * @return array
+     */
+    public function parseDictionary(string $data): array {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            return $this->parseDictionaryImpl($tokens, null)[0];
+        } else {
+            return [];
+        }
+    }
+
+
+    /**
+     * The main method of the parser. It looks on the first token
+     * and decides on the following action based on the delimiter
+     * and the value of the token.
+     *
+     * @param \Generator $tokens
+     * @return array
+     */
+    protected function parseImpl(\Generator $tokens): array {
+        // Get a token and the bounding delimiter
+        $tokenData = $tokens->current();
+        $tokens->next();
+        list($token, $delimiter) = $tokenData;
+        $empty = $token->empty();
+        switch ($delimiter) {
+            case $this->delimiters[self::DICT_START]:
+                // The opening delimiter of a dictionary cannot be preceded by a nonempty token
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                // Start parsing the string as a dictionary
+                return $this->parseDictionaryImpl($tokens, $this->delimiters[self::DICT_END]);
+            case $this->delimiters[self::LIST_START]:
+                // The opening delimiter of a list cannot be preceded by a nonempty token
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                // Start parsing the string as a list
+                return $this->parseListImpl($tokens, $this->delimiters[self::LIST_END]);
+            default:
+                // Otherwise process the obtained token
+                return [$this->process($token), $empty, $delimiter];
+        }
+    }
+
+    /**
+     * A helper function that checks if a list of a dictionary is followed
+     * directly by another delimiter (or the end of the string)
+     *
+     * @param \Generator $tokens
+     * @param string|null $current
+     * @return string|null            the next delimiter or null if none
+     */
+    private function checkNextDelimiter(\Generator $tokens, ?string $current): ?string {
+        if ($current && $tokens->valid()) {
+            list($token, $next) = $tokens->current();
+            $token->empty() or $this->raiseUnexpectedToken($this->offset(), $current, $next);
+            $tokens->next();
+            return $next;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Processes the tokens into a list
+     *
+     * @param \Generator $tokens
+     * @param string|null $endDelimiter
+     * @return array
+     */
+    protected function parseListImpl(\Generator $tokens, ?string $endDelimiter): array {
+        $result = [];
+        do {
+            // Get a value to add to the list
+            list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+            switch ($delimiter) {
+                case $this->delimiters[self::SEP]:
+                    $result[] = $value;
+                    break;
+                case $endDelimiter:
+                    // Add an empty element only if there was a comma
+                    if (!$empty || $result) {
+                        $result[] = $value;
+                    }
+                    // The end of the list - check if not followed by a non-empty token
+                    $delimiter = $this->checkNextDelimiter($tokens, $delimiter);
+                    return [$result, false, $delimiter];
+                default:
+                    // Only list item separators are allowed here
+                    $this->raiseUnexpectedDelimiter(
+                        $delimiter,
+                        $this->offset(),
+                        $this->delimiters[self::SEP] . $endDelimiter
+                    );
+            }
+        } while ($tokens->valid());
+        // The string ended with a comma - append an empty string unless
+        // the list is expected to end with a delimiter
+        if ($endDelimiter) $this->raiseUnexpectedEnd($endDelimiter);
+        $result[] = $this->options['empty'];
+        return [$result, false, null];
+    }
+
+    /**
+     * Processes the tokens into a dictionary
+     *
+     * @param \Generator $tokens
+     * @param string|null $endDelimiter
+     * @return array
+     */
+    protected function parseDictionaryImpl(\Generator $tokens, ?string $endDelimiter): array {
+        $result = [];
+        do {
+            // Get the key
+            list($key, $delimiter) = $tokens->current();
+            $key = strval($key);
+            $tokens->next();
+            if ($delimiter == $this->delimiters[self::KVSEP]) {
+                // The key-value separator is found - find the corresponding value
+                if ($tokens->valid()) {
+                    list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+                } else {
+                    // The end of the string - the value is empty
+                    $delimiter = null;
+                    $empty = true;
+                }
+                // Replace an empty token with the empty value
+                $result[$key] = $empty ? (
+                $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE]
+                ) : $value;
+            } elseif ($key) {
+                // When no key-value separator, then do nothing it the key is empty
+                // In other words: ignore trailing commas
+                $result[$key] = $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE];
+            }
+            // Check if the current delimiter is a correct one
+            if ($delimiter == $endDelimiter) {
+                $delimiter = $this->checkNextDelimiter($tokens, $delimiter);
+                return [$result, false, $delimiter];
+            } elseif ($delimiter != $this->delimiters[self::SEP]) {
+                $this->raiseUnexpectedDelimiter(
+                    $delimiter,
+                    $this->offset(),
+                    $this->delimiters[self::SEP] . $endDelimiter
+                );
+            }
+        } while ($tokens->valid());
+        // Trailing commas are ok for objects
+        return [$result, false, null];
+    }
+}
diff --git a/extension/Classes/Core/Parser/SimpleParser.php b/extension/Classes/Core/Parser/SimpleParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..4981cd5f9fa0b4bfcfa31fb1460adff5dd6577bb
--- /dev/null
+++ b/extension/Classes/Core/Parser/SimpleParser.php
@@ -0,0 +1,168 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Represents a number prefixed with a sign. This class is used
+ * to treat such values differently from absolute numbers.
+ * This class implements `JsonSerializable`, so that it is
+ * treated nicely by `json_encode()`.
+ * @package qfq
+ */
+class SignedNumber implements \JsonSerializable {
+
+    /** @var int|float the value of the number */
+    public $value;
+
+    public function __construct($value) {
+        $this->value = $value;
+    }
+
+    public function __toString(): string {
+        return $this->value > 0 ? "+$this->value" : "$this->value";
+    }
+
+    public function jsonSerialize() {
+        return $this->value;
+    }
+}
+
+/**
+ * Class SimpleParser
+ *
+ * A basic parser that splits the provided string at unescaped
+ * and unquoted delimiters. Token processing recognizes numbers
+ * and specials values.
+ *
+ * @package qfq
+ */
+class SimpleParser extends StringTokenizer {
+
+    /** @var string Option key: replace numeric strings with numbers */
+    const OPTION_PARSE_NUMBERS = 'parse-numbers';
+
+    /** @var string Option key: convert +num and -num to an instance of SignedNumber */
+    const OPTION_KEEP_SIGN = 'keep-sign';
+
+    /** @var string Option key: empty keys will be assigned their names are values */
+    const OPTION_KEY_IS_VALUE = 'key-is-value';
+
+    /** @var string Option key: the value assigned to empty tokens */
+    const OPTION_EMPTY_VALUE = 'empty';
+
+    /** @var array a configuration of the parser */
+    public array $options = [
+        self::OPTION_KEEP_SIGN => false,     // if true, tokens "+number" and "-number" are converted to instances of SignedNumber
+        self::OPTION_KEY_IS_VALUE => false,  // if true, a key with no value is assigned its name
+        self::OPTION_EMPTY_VALUE => null,    // the value used for empty tokens
+        self::OPTION_PARSE_NUMBERS => true   // if true, tokens are replaced with numbers if possible
+    ];
+
+    /**
+     * @var array A dictionary for special values of tokens. These values are
+     *            used only for tokens for which the property `isString` is false.
+     */
+    public array $specialValues = [
+        'null' => null,
+        'true' => true,
+        'false' => false,
+        'yes' => true,
+        'no' => false
+    ];
+
+
+    public function __construct(string $delimiters, array $options = []) {
+        parent::__construct($delimiters);
+        $this->options = array_merge($this->options, $options);
+    }
+
+    /**
+     * Processes a token into a string, a number, or a special value.
+     * @return mixed
+     */
+    protected function process(Token $token) {
+        $asString = strval($token);
+        if ($token->isString) {
+            return $asString;
+        } elseif ($asString === '') {
+            return $this->options[self::OPTION_EMPTY_VALUE];
+        } elseif ($this->options[self::OPTION_PARSE_NUMBERS] && is_numeric($asString)) {
+            if (preg_match("/^[+-]?\d+$/", $asString)) {
+                $value = intval($asString);
+            } else {
+                $value = floatval($asString);
+            }
+            return ($this->options[self::OPTION_KEEP_SIGN] && ($asString[0] === '+' || $asString[0] === '-'))
+                ? new SignedNumber($value) : $value;
+        } elseif (array_key_exists($asString, $this->specialValues)) {
+            // isset() does not work, because the array has `null` as one of values
+            return $this->specialValues[$asString];
+        } else {
+            return $asString;
+        }
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $delimiter
+     * @param $position
+     * @param $expected
+     */
+    protected static function raiseUnexpectedDelimiter($delimiter, $position, $expected=null): void {
+        $msg = "An unexpected '$delimiter' at position $position";
+        if ($expected) {
+            $msg .= " while expecting " . implode(' or ', str_split($expected));
+        }
+        throw new \RuntimeException($msg);
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $expected
+     */
+    protected static function raiseUnexpectedEnd($expected): void {
+        throw new \RuntimeException("An unexpected end while searching for '$expected'");
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $position
+     * @param $before
+     * @param $after
+     */
+    protected static function raiseUnexpectedToken($position, $before, $after): void {
+        $msg = "An unexpected token at $position";
+        $extra = [];
+        if ($before) $extra[] = " before '$before'";
+        if ($after) $extra[] = " after '$after'";
+        $msg .= implode('and ', $extra);
+        throw new \RuntimeException($msg);
+    }
+
+    /**
+     * Parses the provided string into a list of values separated by delimiters.
+     *
+     * @param string $data
+     * @return mixed
+     */
+    public function parse(string $data) {
+        return iterator_to_array($this->iterate($data));
+    }
+
+    /**
+     * Iterates over pieces of a string separated by unescaped delimiters.
+     *
+     * @param string $data
+     * @return \Generator
+     */
+    public function iterate(string $data): \Generator {
+        foreach ($this->tokenized($data) as $token) {
+            yield $this->process($token[0]);
+        }
+    }
+}
diff --git a/extension/Classes/Core/Parser/StringTokenizer.php b/extension/Classes/Core/Parser/StringTokenizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..be3bf3093eb68f6c9165890835f158a7d7911b2f
--- /dev/null
+++ b/extension/Classes/Core/Parser/StringTokenizer.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Class StringTokenizer
+ *
+ * This class is used to parse a string into a sequence of tokens
+ * bound by provided delimiters. While parsing the string, pieces
+ * of a token are generated each time a delimiter, a quote or
+ * a backslash is found. They are joined together and yielded
+ * once an unescaped delimiter is encountered or the end of
+ * the string is reached.
+ *
+ * @project qfq
+ */
+class StringTokenizer {
+
+    /** @var string a regexp pattern to match delimiters */
+    private string $delimitersPattern;
+
+    /** @var int the offset for the current token piece */
+    private int $currentOffset = 0;
+
+    /** @var TokenBuilder the object for building a token */
+    protected TokenBuilder $tokenBuilder;
+
+
+    public function __construct(string $delimiters) {
+        $escapedDelimiters = str_replace(
+            ['[', ']', '/', '.'],
+            ['\\[', '\\]', '\\/', '\\.'],
+            $delimiters
+        );
+        $this->delimitersPattern = "/[$escapedDelimiters\\\\\"']/";
+        $this->tokenBuilder = new TokenBuilder();
+    }
+
+    /**
+     * The offset of the last found delimiter or -1 otherwise
+     * @return int
+     */
+    public function offset(): int {
+        return $this->currentOffset-1;
+    }
+
+    /**
+     * Iterates over unescaped delimiters and quotes from a provided string.
+     * At each iteration returns a delimiter or null when the end of the
+     * string is reached.
+     *
+     * Examples
+     * delimiters: ,:|
+     *    ab:cd, ef|gh    -->  : , | (null)    (token pieces: 'ab' 'cd' 'ef' 'gh')
+     *    ab\:c d,"e:f"   -->  , " : " (null)  (token pieces: 'ab:c d' '' 'e' 'f' '')
+     *    ab\\:cd,' '     -->  : , (null)      (token pieces: 'ab\' 'cd' ' ')
+     *
+     * @param string $data   the searched string
+     * @return \Generator    delimiters
+     */
+    protected function unescapedDelimitersAndQuotes(string $data): \Generator {
+        // Reset the token builder and offset
+        $this->tokenBuilder->reset();
+        $this->currentOffset = 0;
+        // Match all delimiters, including escaped and inside quotes
+        if (preg_match_all($this->delimitersPattern, $data, $delimiters, PREG_OFFSET_CAPTURE)) {
+            // If non-empty, $delimiters is an array with one element:
+            // a list of pairs [delimiter, offset]
+            $delimiters = $delimiters[0];
+            $tokenData = current($delimiters);
+            while ($tokenData) {
+                list($delimiter, $offset) = $tokenData;
+                if ($delimiter == '\\') {
+                    // The next character is escaped. If it is a delimiter,
+                    // then we will ignore it and continue the search.
+                    $tokenData = next($delimiters);
+                    if (!$tokenData) {
+                        // No more delimiters - we have reached the end of the string.
+                        // We return the remaining part of the string outside the loop.
+                        break;
+                    } elseif ($tokenData[1] == $offset + 1) {
+                        // This delimiter or quote is escaped by the backslash
+                        if ($tokenData[0] != '\\') {
+                            $this->tokenBuilder->append(substr($data, $this->currentOffset, $offset-$this->currentOffset));
+                            $this->currentOffset = $tokenData[1];
+                        }
+                        $tokenData = next($delimiters);
+                        continue;
+                    }
+                }
+                // An unescaped delimiter has been found
+                $this->tokenBuilder->append(substr($data, $this->currentOffset, $offset-$this->currentOffset));
+                $this->currentOffset = $offset + 1;
+                yield $delimiter;
+                $tokenData = next($delimiters);
+            }
+        }
+    }
+
+    /**
+     * Iterates over unescaped and unquoted delimiters from a provided string.
+     * Unescaped quotes must match and are not included in the resulting token.
+     * At each iteration returns a pair consisting of
+     * - the token generated from the substring bound by the previous
+     *   and the current delimiter
+     * - the delimiter
+     * Note that the offset of the current delimiter is given by the offset()
+     * method. The delimiter is null when the end of the string is reached.
+     *
+     * Examples
+     * delimiters: ,:|
+     *    ab:cd, ef|gh    -->  ['ab', ':'] ['cd', ','] ['ef', '|'] ['gh', (null)]
+     *    ab\:c d,"e:f"   -->  ['ab:c d', ','] ['e f', (null)]
+     *    ab\\:cd,' '     -->  ['ab\', ':'] ['cd', ','] [' ', (null)]
+     *
+     * @param string $data   the string to search for delimiters
+     * @return \Generator    pairs [token, delimiter]
+     */
+    public function tokenized(string $data): \Generator {
+        // Iterate over all unescaped delimiters
+        $delimitersAndQuotes = $this->unescapedDelimitersAndQuotes($data);
+        while ($delimitersAndQuotes->valid()) {
+            $delimiter = $delimitersAndQuotes->current();
+            if ($delimiter === '"' || $delimiter === "'") {
+                // We will search for the matching quote and put everything
+                // in between to the token
+                $quote = $delimiter;
+                $this->tokenBuilder->markQuote();
+                while (true) {
+                    // Get next delimiter and check if it is a matching quote
+                    $delimitersAndQuotes->next();
+                    if (!$delimitersAndQuotes->valid()) {
+                        throw new \RuntimeException("An unexpected end while searching for '$delimiter'");
+                    }
+                    $delimiter = $delimitersAndQuotes->current();
+                    if ($delimiter === $quote) {
+                        // We have found a quote - break this loop and continue
+                        // searching for delimiters
+                        $this->tokenBuilder->markQuote();
+                        break;
+                    }
+                    // An quoted delimiter is a part of the token
+                    $this->tokenBuilder->pieces[] = $delimiter;
+                }
+            } else {
+                // An unescaped delimiter: return the current token
+                // and start building the next one
+                yield [$this->tokenBuilder->process(), $delimiter];
+            }
+            $delimitersAndQuotes->next();
+        }
+        // No more delimiters: add the rest of the string and process the token
+        $this->tokenBuilder->pieces[] = substr($data, $this->currentOffset);
+        $token = $this->tokenBuilder->process();
+        $token->empty() or yield [$token, null];
+    }
+}
diff --git a/extension/Classes/Core/Parser/Token.php b/extension/Classes/Core/Parser/Token.php
new file mode 100644
index 0000000000000000000000000000000000000000..fe9c43a28aea03bb77b4272576f17cbbd29c5f99
--- /dev/null
+++ b/extension/Classes/Core/Parser/Token.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A token returned by StringTokenizer when parsing a string.
+ * It represents a part of the input string except that unescaped quotes
+ * and backslashes before delimiters and quotes are removed.
+ * @package qfq
+ */
+class Token {
+    /**
+     * @var string The string wrapped by the token.
+     */
+    public string $value;
+
+    /**
+     * @var bool True if the token must be treated as a string
+     *           (for instance it was surrounded with quotes)
+     */
+    public bool $isString;
+
+    public function __construct(string $value, bool $isString) {
+        $this->value = $value;
+        $this->isString = $isString;
+    }
+
+    /**
+     * Returns true when the token is empty. In particular, this means
+     * that there were no quotes in the token.
+     * @return bool
+     */
+    public function empty() : bool {
+        return $this->value === '' && !$this->isString;
+    }
+
+    public function __toString(): string {
+        return $this->value;
+    }
+}
diff --git a/extension/Classes/Core/Parser/TokenBuilder.php b/extension/Classes/Core/Parser/TokenBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..92f714187c9b953b6feb5abf1e8e403237769714
--- /dev/null
+++ b/extension/Classes/Core/Parser/TokenBuilder.php
@@ -0,0 +1,101 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A helper class used by StringTokenizer to build a token. It contains extra information
+ * that are necessary to properly process the token into a string.
+ * @package qfq
+ */
+class TokenBuilder {
+
+    /**
+     * @var array A list of substrings that form the token. Splits occur at escaped
+     * delimiters and unescaped quotes so that they do not appear in a final string.
+     */
+    public array $pieces = [];
+
+    /**
+     * @var int The current total length of the token pieces
+     */
+    public int $length = 0;
+
+    /**
+     * @var int the offset of the first unescaped quote
+     */
+    public int $firstQuoteOffset = -1;
+
+    /**
+     * @var int the offset of the last unescaped quote
+     */
+    public int $lastQuoteOffset = -1;
+
+    /**
+     * Returns true when an unescaped quote has been found
+     * while creating the token.
+     * @return bool
+     */
+    public function hasQuotes(): bool {
+        return $this->firstQuoteOffset >= 0;
+    }
+
+    /**
+     * Resets the builder to its initial state
+     * @return void
+     */
+    public function reset()
+    {
+        $this->pieces = [];
+        $this->length = 0;
+        $this->firstQuoteOffset = -1;
+        $this->lastQuoteOffset = -1;
+    }
+
+    /**
+     * Processes the data to a token and resets the builder
+     * @return Token
+     */
+    public function process(): Token {
+        // Combine all the pieces and trim the resulting string,
+        // but keep whitespaces that were surrounded by quotes
+        $value = implode('', $this->pieces);
+        if ($this->hasQuotes()) {
+            $value = ltrim(substr($value, 0, $this->firstQuoteOffset)) .
+                     substr($value, $this->firstQuoteOffset, $this->lastQuoteOffset-$this->firstQuoteOffset) .
+                     rtrim(substr($value, $this->lastQuoteOffset));
+        } else {
+            $value = trim($value);
+        }
+        $token = new Token($value, $this->hasQuotes());
+        $this->reset();
+        return $token;
+    }
+
+    /**
+     * Adds a piece of a token and updates the builder status
+     * @param string $data
+     * @return void
+     */
+    public function append(string $data) {
+        $this->pieces[] = $data;
+        $this->length += strlen($data);
+    }
+
+    /**
+     * Notifies the builder that a quote has been encountered.
+     * The builder updates offsets of quotes accoringly.
+     * @return void
+     */
+    public function markQuote() {
+        if ($this->firstQuoteOffset < 0) {
+            $this->firstQuoteOffset = $this->length;
+        } else {
+            $this->lastQuoteOffset = $this->length;
+        }
+    }
+}
diff --git a/extension/Classes/Core/QuickFormQuery.php b/extension/Classes/Core/QuickFormQuery.php
index 58088cb815e222d53826f4e0ed76c3d0925ff380..907076b2d3b00f9a4ce7adbb0eac90c7c6ae8412 100644
--- a/extension/Classes/Core/QuickFormQuery.php
+++ b/extension/Classes/Core/QuickFormQuery.php
@@ -172,6 +172,20 @@ class QuickFormQuery {
         $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3);
         $this->store->setVar(TYPO3_TT_CONTENT_SUBHEADER, $t3data[T3DATA_SUBHEADER], STORE_TYPO3);
 
+        // Adds line numbers together with level to TYPO3 store
+        // E.g. [parsed.1 => 2, parsed.1.2 => 4, ...]
+        foreach ($btp->reportLines as $key => $value) {
+            $this->store->setVar(TYPO3_TOKEN_REPORT_LINE . '.' . $key, $value, STORE_TYPO3);
+        }
+
+        // Check if aliases were used
+        if (isset($btp->aliases)) {
+            // Adds aliases together with level to TYPO3 store
+            // E.g. [alias.1 => "myAlias", alias.1.2 => "mySecondAlias", ...]
+            foreach ($btp->aliases as $key => $value) {
+                $this->store->setVar(TOKEN_ALIAS . '.' . $key, $value, STORE_TYPO3);
+            }
+        }
 
         $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
         $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
@@ -603,7 +617,10 @@ class QuickFormQuery {
             $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
             $recordDirty = array();
             $rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg);
-            if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) {
+
+            // Switch to READONLY
+            if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER)
+                && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) {
                 $this->formSpec[F_MODE_GLOBAL] = F_MODE_READONLY;
             }
         }
@@ -1578,7 +1595,6 @@ class QuickFormQuery {
             F_DO_NOT_LOG_COLUMN,
 
             FE_FILE_MAX_FILE_SIZE,
-
             F_FE_DATA_PATTERN_ERROR_SYSTEM,  // Not a classical element to overwrite by form definition, but should be copied to detect changes per custom setting.
         ];
 
diff --git a/extension/Classes/Core/Report/Report.php b/extension/Classes/Core/Report/Report.php
index c5dad53c847bcc7ace1a5be6d0b1507dcf12cfe1..ce3b53ea6aa9d562efaae067468b4751d2cad035 100644
--- a/extension/Classes/Core/Report/Report.php
+++ b/extension/Classes/Core/Report/Report.php
@@ -34,10 +34,15 @@ use IMATHUZH\Qfq\Core\Helper\OnString;
 use IMATHUZH\Qfq\Core\Helper\Path;
 use IMATHUZH\Qfq\Core\Helper\Sanitize;
 use IMATHUZH\Qfq\Core\Helper\Support;
+use IMATHUZH\Qfq\Core\Parser\MixedTypeParser;
+use IMATHUZH\Qfq\Core\Parser\SignedNumber;
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
 use IMATHUZH\Qfq\Core\Store\Sip;
 use IMATHUZH\Qfq\Core\Store\Store;
 use IMATHUZH\Qfq\Core\Typo3\T3Handler;
 
+use Firebase\JWT\JWT;
+
 const DEFAULT_QUESTION = 'question';
 const DEFAULT_ICON = 'icon';
 const DEFAULT_BOOTSTRAP_BUTTON = 'bootstrapButton';
@@ -323,6 +328,27 @@ class Report {
         if (!empty($this->frArray[$index])) {
             throw new \UserReportException ("Double definition: $index is defined more than once.", ERROR_DOUBLE_DEFINITION);
         }
+
+        $alias = TOKEN_ALIAS . "." . $level;
+        $alias = $this->store->getVar($alias, STORE_TYPO3);
+
+        // Throw exception if alias is numeric
+        // E.g. 10, 10.20
+        if (1 === preg_match('/^([0-9\.])+$/', $alias)) {
+            throw new \UserReportException ("Numeric alias detected: $alias cannot be used in report notation 'alias'", ERROR_NUMERIC_ALIAS);
+        }
+
+        // Throw exception if this alias was already used
+        if (!empty($alias)) {
+
+            // Checks if this alias was already used by a different level
+            if (!empty($this->aliases) && in_array($alias, $this->aliases) && array_search($alias, $this->aliases) != $level) {
+                throw new \UserReportException ("Double definition: $alias is defined more than once.", ERROR_DOUBLE_DEFINITION);
+            } else {
+                $this->aliases[$level] = $alias;
+            }
+        }
+
         // store complete line reformatted in frArray
         $this->frArray[$index] = $value;
 
@@ -575,9 +601,13 @@ class Report {
             // Set debug, if one is specified else keep the parent one.
             $lineDebug = $this->getValueParentDefault(TOKEN_DEBUG, $full_super_level, $fullLevel, $cur_level, 0);
 
+            // Get line number of current SQL statement from TYPO3 store
+            $reportLine = $this->store->getVar(TYPO3_TOKEN_REPORT_LINE . '.' . $fullLevel, STORE_TYPO3);
+
             // Prepare Error reporting
             $this->store->setVar(SYSTEM_SQL_RAW, $this->frArray[$fullLevel . "." . TOKEN_SQL], STORE_SYSTEM);
             $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, $fullLevel, STORE_SYSTEM);
+            $this->store->setVar(SYSTEM_REPORT_LINE, $reportLine, STORE_SYSTEM);
 
             // Prepare SQL: replace variables. Actual 'line.total' or 'line.count' will recalculated: don't replace them now!
             unset($this->variables->resultArray[$fullLevel . ".line."][LINE_TOTAL]);
@@ -1341,6 +1371,54 @@ class Report {
                     $content .= Support::encryptDoubleCurlyBraces(FormAsFile::renderColumnFormJson($columnValue, $dbQfq));
                     break;
 
+                // Author: Krzysztof Putyra
+                case COLUMN_JWT:
+                    /* Converts a string
+                     *  claim1:value, claim2:value, ... | key | alg
+                     * into a json web token. Parameters:
+                     * - alg    the name of the signing algorithm (default: HS256)
+                     * - key    the secret key used by the signing algorithm
+                     * Standard claims with an extended interpretation of values:
+                     * - iss    the issuer of the token (default: qfq)
+                     * - iat    the timestamp the token has been issued (default: current)
+                     * - exp    the expiration timestamp or the number of seconds till invalid (if prefixed with '+')
+                     * - nbf    the timestamp from when or (if prefixed with '+') the number of seconds after which the token is valid
+                     */
+
+                    // Split the column into |-separated sections
+                    $parser = new SimpleParser('|', [
+                        SimpleParser::OPTION_EMPTY_VALUE => ''
+                    ]);
+                    $splitContent = $parser->parse($columnValue);
+                    // Check that key is provided
+                    if (count($splitContent) < 2) {
+                        throw new \UserReportException("JWT requires a secret key, but it is missing");
+                    }
+
+                    // Parse the payload
+                    $currentTime = time();
+                    $parser = new MixedTypeParser(null, [
+                        SimpleParser::OPTION_KEEP_SIGN => true
+                    ]);
+                    $claims = array_merge(
+                        ['iss' => 'qfq', 'iat' => $currentTime],
+                        $parser->parseDictionary($splitContent[0])
+                    );
+                    foreach (['exp', 'nbf', 'iat'] as $claim) {
+                        $value = $claims[$claim] ?? 0;
+                        if ($value instanceof SignedNumber) {
+                            $claims[$claim] = $value->value + $currentTime;
+                        }
+                    }
+
+                    // Create the token
+                    $content .= JWT::encode(
+                        $claims,
+                        $splitContent[1],
+                        $splitContent[2] ?? 'HS256'
+                    );
+                    break;
+
                 // Author: Enis Nuredini
                 case COLUMN_ENCRYPT:
                     $encryptionMethodColumn = $this->store->getVar(SYSTEM_ENCRYPTION_METHOD, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
diff --git a/extension/Classes/Core/Report/Variables.php b/extension/Classes/Core/Report/Variables.php
index 834614f3fc08d8e3a8ed1fd575ac74a18bbb2b36..a9ee655a8d5a7cc2a3e7900ea46ab0427b12a498 100644
--- a/extension/Classes/Core/Report/Variables.php
+++ b/extension/Classes/Core/Report/Variables.php
@@ -25,6 +25,7 @@ namespace IMATHUZH\Qfq\Core\Report;
 
 use IMATHUZH\Qfq\Core\Evaluate;
 use IMATHUZH\Qfq\Core\Helper\OnString;
+use IMATHUZH\Qfq\Core\Store\Store;
 use IMATHUZH\Qfq\Core\Store\T3Info;
 
 
@@ -78,7 +79,14 @@ class Variables {
         // Process all {{x[.x].name}}
 //        $str = preg_replace_callback('/{{\s*(([0-9]+.)+[a-zA-Z0-9_.]+)\s*}}/', 'self::replaceVariables', $text);
 //        $str = preg_replace_callback('/{{\s*(([0-9]+.)+[a-zA-Z0-9_.]+)(:.*)*\s*}}/', 'self::replaceVariables', $text);
-        $str = preg_replace_callback('/{{\s*(([0-9]+.)+[a-zA-Z0-9_.]+)(:[a-zA-Z-]*)*\s*}}/', 'self::replaceVariables', $text);
+
+        // Report notation 'numeric'
+        // E.g. {{10.line.count}}, {{10.20.line.count}}
+        // $str = preg_replace_callback('/{{\s*(([0-9]+.)+[a-zA-Z0-9_.]+)(:[a-zA-Z-]*)*\s*}}/', 'self::replaceVariables', $text);
+
+        // Report notation 'alias'
+        // E.g. {{myAlias.line.count}}, {{myAlias10.line.count}}, {{10myAlias.line.count}}
+        $str = preg_replace_callback('/{{\s*(([a-zA-Z0-9_-]*[0-9.]*.)[.][a-zA-Z0-9_.]+)+(:[a-zA-Z-]*)*\s*}}/', 'self::replaceVariables', $text);
 
         // Try the Stores
         return $this->eval->parse($str, null, null, $dummyStack, $dummyStore, $frCmd);
@@ -96,12 +104,41 @@ class Variables {
      */
     public function replaceVariables($matches): string {
 
-        // $matches[0]: {{10.20.<columnname>::u:}}
-        // $matches[1]: 10.20.<columnname>
-        // $matches[2]: 10.20
-        // $matches[3]: ::u:
+        // Report notation 'numeric
+        //  $matches[0]: {{10.20.<columnname>::u}}
+        //  $matches[1]: 10.20.<columnname>
+        //  $matches[2]: 10.20
+        //  $matches[3]: :u
+
+        // Report notation 'alias'
+        //  $matches[0]: {{myAlias.<columnname>::u}}
+        //  $matches[1]: myAlias.<columnname>
+        //  $matches[2]: myAlias
+        //  $matches[3]: :u
         $data = $matches[0];
 
+        // Isolate first token as possible alias
+        $alias = strtok($matches[2], ".");
+
+        // No numeric value implies that an alias was used
+        if (!is_numeric($alias)) {
+            // Get typo3 store
+            // Aliases are saved like [alias.1 => "myAlias", alias.1.2 => "mySecondAlias", ...]
+            $storeT3 = Store::getStore(STORE_TYPO3);
+            // Check for matching value of $alias
+            $match = array_search($alias, $storeT3, true);
+
+            // Replacement only if matching alias was found
+            if (!empty($match)) {
+                // Extract level from key
+                $level = substr($match, strpos($match, '.') + 1);
+
+                $matches[0] = str_replace($alias, $level, $matches[0]);
+                $matches[1] = str_replace($alias, $level, $matches[1]);
+                $matches[2] = $level . '.';
+            }
+        }
+
         // index of last '.'
         $pos = strrpos($matches[1], ".");
         if ($pos !== false) {
@@ -116,6 +153,15 @@ class Variables {
                     $arr = explode(':', $matches[3]);
                     $data = OnString::escape($arr[1] ?? '', $this->resultArray[$fullLevel][$varName], $rcFlagWipe);
                 }
+                // This is for the specific case, that the variable references its own level
+                // E.g. myAlias { \n sql = SELECT '{{myAlias.line.count}}' \n }
+                // Note: This is only used for line.count and line.total, because non-existing variables must stay unchanged
+                // E.g. myAlias { \n sql = SELECT 1 \n } \n { \n sql = SELECT '{{myAlias.varName}}' \n }
+                // '{{myAlias.varName}}' will not be changed to '{{1.varName}}'
+            } elseif ($varName === LINE_COUNT || $varName === LINE_TOTAL) {
+                // myAlias needs to be replaced by the level
+                // E.g. {{1.2.line.count}}
+                $data = $matches[0];
             }
         }
 
diff --git a/extension/Classes/External/AutoCron.php b/extension/Classes/External/Auto-Cron.php
similarity index 100%
rename from extension/Classes/External/AutoCron.php
rename to extension/Classes/External/Auto-Cron.php
diff --git a/extension/Tests/Unit/Core/BodytextParserTest.php b/extension/Tests/Unit/Core/BodytextParserTest.php
index b069ba46657ff45d209644b50d5d7ef48c448d62..09487b62e073748079d053840bc378b20a88078c 100644
--- a/extension/Tests/Unit/Core/BodytextParserTest.php
+++ b/extension/Tests/Unit/Core/BodytextParserTest.php
@@ -27,12 +27,40 @@ final class BodytextParserTest extends TestCase {
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Simple row, nothing to remove
+        $given = "{\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple row, nothing to remove
+        $given = "myAlias {\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Several rows, remove all but one
         $given = "\n#some comments\n10.sql = SELECT 'Hello World'\n\n    \n   #more comment";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Several rows, remove all but one
+        $given = "\n#some comments\n{\nsql = SELECT 'Hello World'\n\n    \n   #more comment\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Several rows, remove all but one
+        $given = "\n#some comments\nmyAlias {\nsql = SELECT 'Hello World'\n\n    \n   #more comment\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Several rows, all to remove
         $given = "\n#some comments\n\n\n    \n   #more comment";
         $expected = "";
@@ -45,66 +73,220 @@ final class BodytextParserTest extends TestCase {
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Join a line
+        $given = "\n{\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'more content' WHERE help=1";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Join a line
+        $given = "\n    myAlias     {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'more content' WHERE help=1";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Join several lines, incl. form
         $given = "\n10.sql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n 20.head = <table>\n 30.sql     =   SELECT\n col1,\n col2, \n col3\n  # Query stops here\nform = Person\n";
         $expected = "10.sql = SELECT 'Hello World', 'more content' WHERE help=1\n20.head = <table>\n30.sql     =   SELECT col1, col2, col3\nform = Person";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Join several lines, incl. form
+        $given = "\n{\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}\n{\nhead = <table>\n}\n{\nsql     =   SELECT\n col1,\n col2, \n col3\n  # Query stops here\n}\nform = Person\n";
+        $expected = "1.sql = SELECT 'Hello World', 'more content' WHERE help=1\n2.head = <table>\n3.sql     =   SELECT col1, col2, col3\nform = Person";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Join several lines, incl. form
+        $given = "\nmyAlias {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}\n   mySecondAlias  {\nhead = <table>\n}\nmyThirdAlias  {\nsql     =   SELECT\n col1,\n col2, \n col3\n  # Query stops here\n}\nform = Person\n";
+        $expected = "1.sql = SELECT 'Hello World', 'more content' WHERE help=1\n2.head = <table>\n3.sql     =   SELECT col1, col2, col3\nform = Person";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one.
         $given = "10{\nsql = SELECT 'Hello World'\n}\n";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one.
+        $given = "{\nsql = SELECT 'Hello World'\n}\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one.
+        $given = "myAlias {\nsql = SELECT 'Hello World'\n}\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one. No LF at the end
         $given = "10{\nsql = SELECT 'Hello World'\n}";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one. No LF at the end
+        $given = "{\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one. No LF at the end
+        $given = "myAlias {\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression, one, added some white spaces
         $given = "\n\n10 { \n \n   sql    =    SELECT 'Hello World'     \n\n    }\n\n";
         $expected = "10.sql    =    SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression, one, added some white spaces
+        $given = "\n\n { \n \n   sql    =    SELECT 'Hello World'     \n\n    }\n\n";
+        $expected = "1.sql    =    SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression, one, added some white spaces
+        $given = "\n\n myAlias  { \n \n   sql    =    SELECT 'Hello World'     \n\n    }\n\n";
+        $expected = "1.sql    =    SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: multiple, simple
         $given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n}\n30 {\nsql='Hello world3'\n}\n";
         $expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n30.sql='Hello world3'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: multiple, simple
+        $given = "{\nsql = SELECT 'Hello World'\n}\n{\nsql='Hello world2'\n}\n {\nsql='Hello world3'\n}\n";
+        $expected = "1.sql = SELECT 'Hello World'\n2.sql='Hello world2'\n3.sql='Hello world3'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: multiple, simple
+        $given = "myAlias {\nsql = SELECT 'Hello World'\n}\nmyAlias10{\nsql='Hello world2'\n}\n 10myAlias{\nsql='Hello world3'\n}\n";
+        $expected = "1.sql = SELECT 'Hello World'\n2.sql='Hello world2'\n3.sql='Hello world3'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: complex
         $given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 { \n sql=SELECT 'Hello World3'\n40 { \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
         $expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n20.30.sql=SELECT 'Hello World3'\n20.30.40.sql = SELECT 'Hello World4'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: complex
+        $given = "{\nsql = SELECT 'Hello World'\n}\n {\nsql='Hello world2'\n { \n sql=SELECT 'Hello World3'\n { \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
+        $expected = "1.sql = SELECT 'Hello World'\n2.sql='Hello world2'\n2.3.sql=SELECT 'Hello World3'\n2.3.4.sql = SELECT 'Hello World4'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: complex
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}\n myAlias2{\nsql='Hello world2'\n myAlias3{ \n sql=SELECT 'Hello World3'\n myAlias4{ \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
+        $expected = "1.sql = SELECT 'Hello World'\n2.sql='Hello world2'\n2.3.sql=SELECT 'Hello World3'\n2.3.4.sql = SELECT 'Hello World4'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // form=...., {{ }}
         $given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 {\nsql=SELECT 'Hello World'\n}\n   form=Person\n";
         $expected = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30.sql=SELECT 'Hello World'\nform=Person";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // form=...., {{ }}
+        $given = "{\nsql = SELECT 'Hello World'\n}\nform = {{form:S}}\n{\nsql = SELECT 'Hello World2'\n}\n {\nsql=SELECT 'Hello World'\n}\n   form=Person\n";
+        $expected = "1.sql = SELECT 'Hello World'\nform = {{form:S}}\n2.sql = SELECT 'Hello World2'\n3.sql=SELECT 'Hello World'\nform=Person";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // form=...., {{ }}
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}\nform = {{form:S}}\nmyAlias2 {\nsql = SELECT 'Hello World2'\n}\n myAlias3{\nsql=SELECT 'Hello World'\n}\n   form=Person\n";
+        $expected = "1.sql = SELECT 'Hello World'\nform = {{form:S}}\n2.sql = SELECT 'Hello World2'\n3.sql=SELECT 'Hello World'\nform=Person";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested: open bracket alone
         $given = "10.sql = SELECT 'Hello World'\n20\n{\nhead=test\n}\n30.sql = SELECT 'Hello World'\n";
         $expected = "10.sql = SELECT 'Hello World'\n20.head=test\n30.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested: open bracket alone
+        $given = "{\nsql = SELECT 'Hello World'\n}\n\n{\nhead=test\n}\n{\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'\n2.head=test\n3.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested: open bracket alone
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}\n\nmySecondAlias{\nhead=test\n}\nmyThirdAlias{\nsql = SELECT 'Hello World'\n}";
+        $expected = "1.sql = SELECT 'Hello World'\n2.head=test\n3.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Single open bracket inside a string.
         $given = "10.sql = SELECT 'Hello { World'";
         $expected = "10.sql = SELECT 'Hello { World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Single open bracket inside a string.
+        $given = "{\nsql = SELECT 'Hello { World'\n}";
+        $expected = "1.sql = SELECT 'Hello { World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Single open bracket inside a string.
+        $given = "myAlias{\nsql = SELECT 'Hello { World'\n}";
+        $expected = "1.sql = SELECT 'Hello { World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Complex test
         $given = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20 {\n   10 {\n      5 {\n         sql = SELECT 'hello world<br>'\n      }\n   }\n}\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT  '20.10<br>'";
         $expected = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20.10.5.sql = SELECT 'hello world<br>'\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT  '20.10<br>'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Complex test
+        $given = "{\nsql = SELECT '[\*]{7} [0-9]{5}<br>'\n}\n{\n   {\n       {\n         sql = SELECT 'hello world<br>'\n  head = Terrific   \n}\n   }\n}\n{\nsql = SELECT 5, '<br>'\n}\n{\nsql = SELECT  '6<br>'\n}";
+        $expected = "1.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n2.3.4.sql = SELECT 'hello world<br>'\n2.3.4.head = Terrific\n5.sql = SELECT 5, '<br>'\n6.sql = SELECT  '6<br>'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Complex test
+        $given = "myAliasOne{\nsql = SELECT '[\*]{7} [0-9]{5}<br>'\n}\nmyAliasTwo{\n   myAliasThree{\n       myAliasFour{\n         sql = SELECT 'hello world<br>'\n  head = Terrific   \n}\n   }\n}\nmyAliasFive  {\nsql = SELECT 5, '<br>'\n}\nmyLastAlias{\nsql = SELECT  '6<br>'\n}";
+        $expected = "1.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n2.3.4.sql = SELECT 'hello world<br>'\n2.3.4.head = Terrific\n5.sql = SELECT 5, '<br>'\n6.sql = SELECT  '6<br>'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
     }
 
     public function testNestingToken() {
@@ -116,48 +298,160 @@ final class BodytextParserTest extends TestCase {
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level curly
+        $given = "{ \n     sql = SELECT 'Hello World' \n , 'next line', \n \n 'end' \n} \n";
+        $expected = "1.sql = SELECT 'Hello World' , 'next line', 'end'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level curly
+        $given = "myAlias{ \n     sql = SELECT 'Hello World' \n , 'next line', \n \n 'end' \n} \n";
+        $expected = "1.sql = SELECT 'Hello World' , 'next line', 'end'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level angle
         $given = "#<\n10 < \n     sql = SELECT 'Hello World'\n>\n";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level angle
+        $given = "#<\n < \n     sql = SELECT 'Hello World'\n>\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level angle
+        $given = "#<\n myAlias< \n     sql = SELECT 'Hello World'\n>\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level angle and single curly
         $given = "#<\n10 < \n  head = data { \n '1','2','3' \n }\n>\n";
         $expected = "10.head = data { '1','2','3' }";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level angle and single curly
+        $given = "#<\n < \n  head = data { \n '1','2','3' \n }\n>\n";
+        $expected = "1.head = data { '1','2','3' }";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level angle and single curly
+        $given = "#<\n myAlias< \n  head = data { \n '1','2','3' \n }\n>\n";
+        $expected = "1.head = data { '1','2','3' }";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level angle and single curly
         $given = "  #   < \n 10 < \n  sql = SELECT 'Hello World' \n>\n";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level angle and single curly
+        $given = "  #   < \n  < \n  sql = SELECT 'Hello World' \n>\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level angle and single curly
+        $given = "  #   myAlias1< \n  myAlias2< \n  sql = SELECT 'Hello World' \n>\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level round bracket
         $given = "  #   ( \n 10 ( \n  sql = SELECT 'Hello World' \n)\n";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level round bracket
+        $given = "  #   ( \n  ( \n  sql = SELECT 'Hello World' \n)\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level round bracket
+        $given = "  #   ( \n  myAlias( \n  sql = SELECT 'Hello World' \n)\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level square bracket
         $given = "  #   [ \n 10 [ \n  sql = SELECT 'Hello World' \n]\n";
         $expected = "10.sql = SELECT 'Hello World'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level square bracket
+        $given = "  #   [ \n  [ \n  sql = SELECT 'Hello World' \n]\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level square bracket
+        $given = "  #   [ \n  myAlias[ \n  sql = SELECT 'Hello World' \n]\n";
+        $expected = "1.sql = SELECT 'Hello World'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: one level angle - garbage
         $given = "  #   < \n 10 { \n  sql = SELECT 'Hello World' \n}\n";
         $expected = "10 {\nsql = SELECT 'Hello World' }";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: one level angle - garbage
+        $given = "  #   < \n  { \n  sql = SELECT 'Hello World' \n}\n";
+        $expected = "{\nsql = SELECT 'Hello World' }";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: one level angle - garbage
+        $given = "  #   < \n  myAlias{ \n  sql = SELECT 'Hello World' \n}\n";
+        $expected = "myAlias{\nsql = SELECT 'Hello World' }";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         // Nested expression: '</script>' is allowed here - with bad implemented parsing, it would detect a nesting token end, which is not the meant.
         $given = "  #   < \n 10 < \n  sql = SELECT 'Hello World' \n head = <script> \n>\n20.tail=</script>\n\n\n30.sql=SELECT 'something'";
         $expected = "10.sql = SELECT 'Hello World'\n10.head = <script>\n20.tail=</script>\n30.sql=SELECT 'something'";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // Nested expression: '</script>' is allowed here - with bad implemented parsing, it would detect a nesting token end, which is not the meant.
+        $given = "  #   < \n  < \n  sql = SELECT 'Hello World' \n head = <script> \n>\n<\ntail=</script>\n>\n<\nsql=SELECT 'something'\n>";
+        $expected = "1.sql = SELECT 'Hello World'\n1.head = <script>\n2.tail=</script>\n3.sql=SELECT 'something'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Nested expression: '</script>' is allowed here - with bad implemented parsing, it would detect a nesting token end, which is not the meant.
+        $given = "  #   < \n  myAlias < \n  sql = SELECT 'Hello World' \n head = <script> \n>\n myAlias2 <\ntail=</script>\n>\nmyAlias3<\nsql=SELECT 'something'\n>";
+        $expected = "1.sql = SELECT 'Hello World'\n1.head = <script>\n2.tail=</script>\n3.sql=SELECT 'something'";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
         $open = '<';
         $close = '>';
         // muliple nesting, unnested rows  inbetween
@@ -211,6 +505,125 @@ EOF;
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
 
+        // Report notation 'alias'
+        // muliple nesting, unnested rows  inbetween
+        $given = <<<EOF
+                # $open
+                
+                $open
+                    head = <h1>
+                $close
+                $open
+                    sql = SELECT ...
+                $close
+                $open
+                  $open
+                    sql = SELECT 3.4
+                    head = <script>
+                    tail = </script>
+                    $open
+                      head = <div>
+                    $close
+                  $close
+                  $open
+                    sql = SELECT 3.6
+                  $close
+                  $open
+                    sql = SELECT 3.7
+                  $close
+                  $open
+                    $open
+                        tail = }
+                        }
+                        (:
+                        }
+                        ]
+                        sql = SELECT 3.8.9
+                        head = {
+                          {
+                          )
+                          {
+                          ;
+                          [
+                    $close
+                    $open
+                      sql = SELECT 3.8.10
+                    $close
+                    $open
+                        sql = SELECT 3.8.11
+                    $close
+                  $close
+                  $open
+                    head = <table>
+                  $close
+                $close
+                $open
+                  sql = SELECT 13
+                $close
+EOF;
+        $expected = "1.head = <h1>\n2.sql = SELECT ...\n3.4.sql = SELECT 3.4\n3.4.head = <script>\n3.4.tail = </script>\n3.4.5.head = <div>\n3.6.sql = SELECT 3.6\n3.7.sql = SELECT 3.7\n3.8.9.tail = } } (: } ]\n3.8.9.sql = SELECT 3.8.9\n3.8.9.head = { { ) { ; [\n3.8.10.sql = SELECT 3.8.10\n3.8.11.sql = SELECT 3.8.11\n3.12.head = <table>\n13.sql = SELECT 13";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // muliple nesting, unnested rows  inbetween
+        $given = <<<EOF
+                # $open
+                
+                myAlias1 $open
+                    head = <h1>
+                $close
+                myAlias2 $open
+                    sql = SELECT ...
+                $close
+                myAlias3 $open
+                  myAlias4 $open
+                    sql = SELECT 3.4
+                    head = <script>
+                    tail = </script>
+                    myAlias4 $open
+                      head = <div>
+                    $close
+                  $close
+                  myAlias5 $open
+                    sql = SELECT 3.6
+                  $close
+                  myAlias6 $open
+                    sql = SELECT 3.7
+                  $close
+                  myAlias7 $open
+                    myAlias8 $open
+                        tail = }
+                        }
+                        (:
+                        }
+                        ]
+                        sql = SELECT 3.8.9
+                        head = {
+                          {
+                          )
+                          {
+                          ;
+                          [
+                    $close
+                    myAlias9 $open
+                      sql = SELECT 3.8.10
+                    $close
+                    myAlias10$open
+                        sql = SELECT 3.8.11
+                    $close
+                  $close
+                  myAlias11$open
+                    head = <table>
+                  $close
+                $close
+                myAlias12$open
+                  sql = SELECT 13
+                $close
+EOF;
+        $expected = "1.head = <h1>\n2.sql = SELECT ...\n3.4.sql = SELECT 3.4\n3.4.head = <script>\n3.4.tail = </script>\n3.4.5.head = <div>\n3.6.sql = SELECT 3.6\n3.7.sql = SELECT 3.7\n3.8.9.tail = } } (: } ]\n3.8.9.sql = SELECT 3.8.9\n3.8.9.head = { { ) { ; [\n3.8.10.sql = SELECT 3.8.10\n3.8.11.sql = SELECT 3.8.11\n3.12.head = <table>\n13.sql = SELECT 13";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
     }
 
     public function testVariousNestingToken() {
@@ -232,6 +645,30 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // level open
+            $given = <<<EOF
+                # $open
+                 $open
+                  sql = SELECT 'Hello World'
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // level open
+            $given = <<<EOF
+                # $open
+                 myAlias $open
+                  sql = SELECT 'Hello World'
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
             // level \n alone
             $given = <<<EOF
                 # $open
@@ -244,6 +681,32 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // level \n alone
+            $given = <<<EOF
+                # $open
+                
+                $open
+                  sql = SELECT 'Hello World'
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // level \n alone
+            $given = <<<EOF
+                # $open
+                
+                myAlias$open
+                  sql = SELECT 'Hello World'
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
             // various linebreaks
             $given = <<<EOF
                 # $open
@@ -259,6 +722,38 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // various linebreaks
+            $given = <<<EOF
+                # $open
+
+                  $open
+
+                  sql = SELECT 'Hello World'
+
+                $close
+
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // various linebreaks
+            $given = <<<EOF
+                # $open
+
+                  myAlias $open
+
+                  sql = SELECT 'Hello World'
+
+                $close
+
+EOF;
+            $expected = "1.sql = SELECT 'Hello World'";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
             // multi line
             $given = <<<EOF
                 # $open
@@ -277,6 +772,42 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // multi line
+            $given = <<<EOF
+                # $open
+                  $open
+                  sql = SELECT 'Hello World'
+                        FROM Person
+
+                        ORDER BY id
+
+                        LIMIT 4
+                   head = <div>
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // multi line
+            $given = <<<EOF
+                # $open
+                  myAlias $open
+                  sql = SELECT 'Hello World'
+                        FROM Person
+
+                        ORDER BY id
+
+                        LIMIT 4
+                   head = <div>
+                $close
+EOF;
+            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
             // mulitple nesting
             $given = <<<EOF
                 # $open
@@ -300,6 +831,58 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // mulitple nesting
+            $given = <<<EOF
+                # $open
+                $open
+                  head = <h1>
+                $close
+                  $open
+                  sql = SELECT 'Hello World'
+                   $open
+                    sql = SELECT 'Hi'
+                    head = <script>
+                    tail = </script>
+                     $open
+                        head = <div>
+                    $close
+
+                  $close
+
+                $close
+
+EOF;
+            $expected = "1.head = <h1>\n2.sql = SELECT 'Hello World'\n2.3.sql = SELECT 'Hi'\n2.3.head = <script>\n2.3.tail = </script>\n2.3.4.head = <div>";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // mulitple nesting
+            $given = <<<EOF
+                # $open
+                myAlias$open
+                  head = <h1>
+                $close
+                  myAlias2$open
+                  sql = SELECT 'Hello World'
+                   myAlias3$open
+                    sql = SELECT 'Hi'
+                    head = <script>
+                    tail = </script>
+                     myAlias4$open
+                        head = <div>
+                    $close
+
+                  $close
+
+                $close
+
+EOF;
+            $expected = "1.head = <h1>\n2.sql = SELECT 'Hello World'\n2.3.sql = SELECT 'Hi'\n2.3.head = <script>\n2.3.tail = </script>\n2.3.4.head = <div>";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
             // muliple nesting, unnested rows  inbetween
             $given = <<<EOF
                 # $open
@@ -325,6 +908,66 @@ EOF;
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
+            // Report notation 'alias'
+            // muliple nesting, unnested rows  inbetween
+            $given = <<<EOF
+                # $open
+                $open
+                  head = <h1>
+                $close
+                $open
+                  sql = SELECT 'Hello World'
+                  $open
+                    sql = SELECT 'Hi'
+                    head = <script>
+                    tail = </script>
+                    $open
+                      head = <div>
+                    $close
+                  $close
+                  $open
+                    sql = SELECT 'After'
+                  $close
+                $close
+                $open
+                  sql = SELECT ...
+                $close
+
+EOF;
+            $expected = "1.head = <h1>\n2.sql = SELECT 'Hello World'\n2.3.sql = SELECT 'Hi'\n2.3.head = <script>\n2.3.tail = </script>\n2.3.4.head = <div>\n2.5.sql = SELECT 'After'\n6.sql = SELECT ...";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
+            // Report notation 'alias' with alias
+            // muliple nesting, unnested rows  inbetween
+            $given = <<<EOF
+                # $open
+                myAlias$open
+                  head = <h1>
+                $close
+                mySecondAlias$open
+                  sql = SELECT 'Hello World'
+                  myThirdAlias$open
+                    sql = SELECT 'Hi'
+                    head = <script>
+                    tail = </script>
+                    myFourthAlias$open
+                      head = <div>
+                    $close
+                  $close
+                  myFifthAlias$open
+                    sql = SELECT 'After'
+                  $close
+                $close
+                mySixthAlias$open
+                  sql = SELECT ...
+                $close
+
+EOF;
+            $expected = "1.head = <h1>\n2.sql = SELECT 'Hello World'\n2.3.sql = SELECT 'Hi'\n2.3.head = <script>\n2.3.tail = </script>\n2.3.4.head = <div>\n2.5.sql = SELECT 'After'\n6.sql = SELECT ...";
+            $result = $btp->process($given);
+            $this->assertEquals($expected, $result);
+
         }
 
     }
@@ -347,6 +990,40 @@ EOF;
         $expected = "10.sql = SELECT 'Hello World', 'p:id=1&grId=2\n20.sql = SELECT ''";
         $result = $btp->process($given);
         $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple row, nothing to remove
+        $given = "{\nsql = SELECT 'Hello World', 'p:id=1&\\\ngrId=2\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        $given = "{\nsql = SELECT 'Hello World', 'p:id=1&  \\\n  grId=2  \n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        $given = "{\nsql = SELECT 'Hello World', 'p:id=1&  \\\n  grId=2  \\\n }\n{\nsql = SELECT ''\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2\n2.sql = SELECT ''";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple row, nothing to remove
+        $given = "myAlias{\nsql = SELECT 'Hello World', 'p:id=1&\\\ngrId=2\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        $given = "myAlias{\nsql = SELECT 'Hello World', 'p:id=1&  \\\n  grId=2  \n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
+
+        $given = "myAlias{\nsql = SELECT 'Hello World', 'p:id=1&  \\\n  grId=2  \\\n }\n{\nsql = SELECT ''\n}";
+        $expected = "1.sql = SELECT 'Hello World', 'p:id=1&grId=2\n2.sql = SELECT ''";
+        $result = $btp->process($given);
+        $this->assertEquals($expected, $result);
     }
 
     /**
@@ -360,6 +1037,14 @@ EOF;
         // Nested: unmatched close bracket
         $btp->process("10.sql = SELECT 'Hello World'\n } \n30.sql = SELECT 'Hello World'\n");
 
+        // Report notation 'alias'
+        // Nested: unmatched close bracket
+        $btp->process("sql = SELECT 'Hello World'\n } \n{\nsql = SELECT 'Hello World'\n}");
+
+        // Report notation 'alias' with alias
+        // Nested: unmatched close bracket
+        $btp->process("sql = SELECT 'Hello World'\n } \nmyAlias{\nsql = SELECT 'Hello World'\n}");
+
     }
 
     /**
@@ -372,6 +1057,237 @@ EOF;
         // Nested: unmatched open bracket
         $btp->process("10.sql = SELECT 'Hello World'\n20 { \n30.sql = SELECT 'Hello World'\n");
 
+        // Report notation 'alias'
+        // Nested: unmatched open bracket
+        $btp->process("{\nsql = SELECT 'Hello World'\n}\n { \n30.sql = SELECT 'Hello World'\n");
+
+        // Report notation 'alias' with alias
+        // Nested: unmatched open bracket
+        $btp->process("myAlias{\nsql = SELECT 'Hello World'\n}\n myAlias2{ \n30.sql = SELECT 'Hello World'\n");
+
     }
 
-}
+    /**
+     *
+     */
+    public function testReportLines() {
+        $btp = new BodytextParser();
+
+        // Simple statement
+        $given = "10.sql = SELECT 'Hello World'";
+        $expected = [10 => 1];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statement, nested
+        $given = "10 {\nsql = SELECT 'Hello World'\n}";
+        $expected = [10 => 2];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statement, nested
+        $given = "{\nsql = SELECT 'Hello World'\n}";
+        $expected = [1 => 2];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statement, nested
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}";
+        $expected = [1 => 2];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statement, multiple unnecessary lines
+        $given = "\n#some comments\n10.sql = SELECT 'Hello World'\n\n    \n   #more comment";
+        $expected = [10 => 3];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statement, multiple unnecessary lines, nested
+        $given = "\n#some comments\n10\n{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment";
+        $expected = [10 => 5];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statement, multiple unnecessary lines, nested
+        $given = "\n#some comments\n\n{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment";
+        $expected = [1 => 5];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statement, multiple unnecessary lines, nested
+        $given = "\n#some comments\n\nmyAlias{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment";
+        $expected = [1 => 5];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // No statement
+        $given = "\n#some comments\n\n\n    \n   #more comment";
+        $expected = [];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statement on multiple lines
+        $given = "\n10.sql = SELECT 'Hello World',\n'more content'\n      WHERE help=1";
+        $expected = [10 => 2];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statement on multiple lines, nested
+        $given = "\n10 {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}";
+        $expected = [10 => 3];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statement on multiple lines, nested
+        $given = "\n {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}";
+        $expected = [1 => 3];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statement on multiple lines, nested
+        $given = "\n myAlias{\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}";
+        $expected = [1 => 3];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements
+        $given = "10.sql = SELECT 'Hello World'\n20.sql = SELECT 'Hello World'\n30.sql = SELECT 'Hello World'";
+        $expected = [10 => 1, 20 => 2, 30 => 3];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements, nested
+        $given = "10 {\nsql = SELECT 'Hello World'\n}\n20 {\nsql = SELECT 'Hello World'\n}\n30 {\nsql = SELECT 'Hello World'\n}";
+        $expected = [10 => 2, 20 => 5, 30 => 8];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statements, nested
+        $given = "{\nsql = SELECT 'Hello World'\n}\n{\nsql = SELECT 'Hello World'\n}\n{\nsql = SELECT 'Hello World'\n}";
+        $expected = [1 => 2, 2 => 5, 3 => 8];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statements, nested
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}\nmyAlias2{\nsql = SELECT 'Hello World'\n}\nmyAlias3{\nsql = SELECT 'Hello World'\n}";
+        $expected = [1 => 2, 2 => 5, 3 => 8];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements, multiple unnecessary lines
+        $given = "\n#some comments\n10.sql = SELECT 'Hello World'\n\n    \n   #more comment \n 20.sql = SELECT 'Hello World'\n #more comment\n\n\n 30.sql=SELECT 'Hello World'";
+        $expected = [10 => 3, 20 => 7, 30 => 11];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements, multiple unnecessary lines, nested
+        $given = "\n#some comments\n10\n{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment\n\n 20 {\nsql = SELECT 'Hello World'\n\n}\n #more comment \n\n\n 30 {\nsql = SELECT 'Hello World'\n\n}";
+        $expected = [10 => 5, 20 => 12, 30 => 19];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statements, multiple unnecessary lines, nested
+        $given = "\n#some comments\n\n{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment\n\n  {\nsql = SELECT 'Hello World'\n\n}\n #more comment \n\n\n  {\nsql = SELECT 'Hello World'\n\n}";
+        $expected = [1 => 5, 2 => 12, 3 => 19];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statements, multiple unnecessary lines, nested
+        $given = "\n#some comments\n\nmyAlias{\nsql = SELECT 'Hello World'\n\n}\n    \n   #more comment\n\n  myAlias2{\nsql = SELECT 'Hello World'\n\n}\n #more comment \n\n\n  myAlias3{\nsql = SELECT 'Hello World'\n\n}";
+        $expected = [1 => 5, 2 => 12, 3 => 19];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements on multiple lines
+        $given = "\n10.sql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n\n 20.sql = SELECT 'Hello World', \n 'some more content'";
+        $expected = [10 => 2, 20 => 6];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Simple statements on multiple lines, nested
+        $given = "\n10 {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}\n\n20\n {\nsql = SELECT 'Hello World', \n 'some more content'\n}";
+        $expected = [10 => 3, 20 => 10];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Simple statements on multiple lines, nested
+        $given = "\n {\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}\n\n{\nsql = SELECT 'Hello World', \n 'some more content'\n}";
+        $expected = [1 => 3, 2 => 9];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Simple statements on multiple lines, nested
+        $given = "\n myAlias{\nsql = SELECT 'Hello World',\n'more content'\n      WHERE help=1\n}\n\nmyAlias2{\nsql = SELECT 'Hello World', \n 'some more content'\n}";
+        $expected = [1 => 3, 2 => 9];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Complex statements
+        $given = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n20.30.sql=SELECT 'Hello World3'\n20.30.40.sql = SELECT 'Hello World4'";
+        $expected = [10 => 1, 20 => 2, '20.30' => 3, '20.30.40' => 4];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Complex statements, nested
+        $given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 { \n sql=SELECT 'Hello World3'\n40 { \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
+        $expected = [10 => 1, 20 => 3, '20.30' => 5, '20.30.40' => 7];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias'
+        // Complex statements: complex
+        $given = "{\nsql = SELECT 'Hello World'\n}\n {\nsql='Hello world2'\n { \n sql=SELECT 'Hello World3'\n { \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
+        $expected = [1 => 2, 2 => 5, '2.3' => 7, '2.3.4' => 9];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+
+        // Report notation 'alias' with alias
+        // Complex statements: complex
+        $given = "myAlias{\nsql = SELECT 'Hello World'\n}\n myAlias2{\nsql='Hello world2'\n myAlias3{ \n sql=SELECT 'Hello World3'\n myAlias4{ \n sql = SELECT 'Hello World4'\n  }  \n  } \n  }  ";
+        $expected = [1 => 2, 2 => 5, '2.3' => 7, '2.3.4' => 9];
+        $btp->process($given);
+        $result = $btp->reportLines;
+        $this->assertEquals($expected, $result);
+    }
+}
\ No newline at end of file
diff --git a/extension/Tests/Unit/Core/BuildFormPlainTest.php b/extension/Tests/Unit/Core/BuildFormPlainTest.php
index 0aab8fdfb1cab35134867d1e4cf55e7ec8479e19..c9733c71eb9ff035a61f1418bf3ae3162307faf3 100644
--- a/extension/Tests/Unit/Core/BuildFormPlainTest.php
+++ b/extension/Tests/Unit/Core/BuildFormPlainTest.php
@@ -40,11 +40,17 @@ class BuildFormPlainTest extends AbstractDatabaseTest {
         $build = new BuildFormPlain([F_DB_INDEX => DB_INDEX_DEFAULT], array(), array(), $this->dbArray);
 
         $formId1 = $build->getFormId();
-        $this->assertMatchesRegularExpression('/qfq-form-[0-9a-f]{13}/', $formId1);
+        $this->assertMatchesRegularExpression('/qfq-form-[0-9]*-[0-9]*-[0-9]*/', $formId1);
 
         $formId2 = $build->getFormId();
         $this->assertEquals($formId1, $formId2);
 
+        /*
+         * 1) IMATHUZH\Qfq\Tests\Unit\Core\BuildFormPlainTest::testGetFormId
+Failed asserting that 'qfq-form-1234--3' matches PCRE pattern "/qfq-form-[0-9a-f]{13}/".
+/home/gitlab-runner/builds/_jtKSCsb/0/typo3/qfq/typo3conf/ext/qfq/Tests/Unit/Core/BuildFormPlainTest.php:43
+FAILURES!
+         */
     }
 
     /**
diff --git a/extension/Tests/Unit/Core/Form/DirtyTest.php b/extension/Tests/Unit/Core/Form/DirtyTest.php
index 3fe8a8c1098c372b81aedfe20d9012d6e3f9df3f..f8a18f000fa7d8e1165cf280d25f6aef6c3ff69b 100644
--- a/extension/Tests/Unit/Core/Form/DirtyTest.php
+++ b/extension/Tests/Unit/Core/Form/DirtyTest.php
@@ -9,7 +9,7 @@
 namespace IMATHUZH\Qfq\Tests\Unit\Core\Form;
 
 use IMATHUZH\Qfq\Core\Database\Database;
- 
+
 use IMATHUZH\Qfq\Core\Form\Dirty;
 use IMATHUZH\Qfq\Core\Store\Session;
 use IMATHUZH\Qfq\Core\Store\Sip;
@@ -17,6 +17,8 @@ use IMATHUZH\Qfq\Tests\Unit\Core\Database\AbstractDatabaseTest;
 
 require_once(__DIR__ . '/../Database/AbstractDatabaseTest.php');
 
+const MSG_RECORD_ALREADY_LOCKED = 'Record already locked';
+
 /*
  * Open to check
  * - FORM_DELETE
@@ -352,7 +354,7 @@ class DirtyTest extends AbstractDatabaseTest {
         // Alice lock again
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -508,7 +510,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -700,8 +702,8 @@ class DirtyTest extends AbstractDatabaseTest {
         // Alice lock again
         $result = $dirty->process();
 
-        $msg = 'The record has already';
-        $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg];
+        $msg = MSG_RECORD_ALREADY_LOCKED;
+        $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
         $result[API_MESSAGE] = substr($result[API_MESSAGE], 0, strlen($msg));
@@ -855,7 +857,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -977,7 +979,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -1021,7 +1023,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = 'Record already locked ';
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -1065,7 +1067,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
@@ -1109,7 +1111,7 @@ class DirtyTest extends AbstractDatabaseTest {
 
         $result = $dirty->process();
 
-        $msg = 'The record has already';
+        $msg = MSG_RECORD_ALREADY_LOCKED;
         $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg];
 
         // cut IP, User and Timestamp
diff --git a/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php b/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae87a6c8b81293ef5dfc1493010e7e04b8ae069b
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\KVPairListParser;
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
+use PHPUnit\Framework\TestCase;
+
+class KVPairListParserTest extends TestCase {
+    public function testEmptyString() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            [],
+            $parser->parse('')
+        );
+    }
+
+    public function testSinglePair() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(['a' => 42], $parser->parse('a:42'));
+    }
+
+    public function testSingleKey() {
+        $parser = new KVPairListParser('|', ':', [
+            SimpleParser::OPTION_KEY_IS_VALUE => true
+        ]);
+        $this->assertEquals(['a' => 'a'], $parser->parse('a:'));
+
+        $parser = new KVPairListParser('|', ':', [
+            SimpleParser::OPTION_KEY_IS_VALUE => false,
+            SimpleParser::OPTION_EMPTY_VALUE => 18
+        ]);
+        $this->assertEquals(['a' => 18], $parser->parse('a:'));
+        $this->assertEquals(['a' => 18], $parser->parse('a'));
+    }
+
+    public function testSimpleList() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab'=>'x','cd'=>'y','ef'=>'z'],
+            $parser->parse('ab:x|cd:y|ef:z')
+        );
+    }
+
+    public function testEscapedSeparators() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab' => 'x','cd:y|ef' => 'z:a'],
+            $parser->parse('ab:x|cd\\:y\\|ef:z\\:a')
+        );
+    }
+
+    public function testQuotedSeparators() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab' => 'x','cd:y| ef' => 'z:a'],
+            $parser->parse('ab:x|"cd:y| ef":z":a"')
+        );
+    }
+
+    public function testIterate() {
+        $parser = new KVPairListParser('|', ':');
+        $iterator = $parser->iterate('a:1|b:2|c:3');
+        $expected = ['a' => 1, 'b' => 2, 'c' => 3];
+        foreach($iterator as $key => $value) {
+            $expectedKey = key($expected);
+            $expectedValue = current($expected);
+            $this->assertSame($expectedKey, $key);
+            $this->assertSame($expectedValue, $value);
+            next($expected);
+        }
+        $this->assertFalse(current($expected));
+    }
+}
diff --git a/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php b/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..66f9f69b78e6afdf219e41fc62e395cf6f167b5c
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php
@@ -0,0 +1,192 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\MixedTypeParser;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class MixedTypeParserTest
+ * @package qfq
+ */
+class MixedTypeParserTest extends TestCase {
+
+    protected function assertGeneratorOutput($generator, $output) {
+        foreach ($output as $value) {
+            $data = $generator->current();
+            $data[0] = strval($data[0]);
+            $this->assertSame($value, $data, "Expecting " . json_encode($value));
+            $generator->next();
+        }
+        $this->assertNull($generator->current());
+    }
+
+    public function testLiteralValues() {
+        $parser = new MixedTypeParser(null, ['empty' => 18]);
+        $data = [
+            'abc' => 'abc',   ' x\\:y  ' => 'x:y',
+            ''  => 18,        '""' => '',
+            ' ' => 18,        '" "' => ' ',
+            '123' => 123,     '"123"' => '123',
+            '12.5' => 12.5,   '"12.5"' => '12.5',
+            'false' => false, '"false"' => 'false',
+            'true' => true,   '"true"' => 'true',
+            'yes' => true,    '"yes"' => 'yes',
+            'no' => false,    '"no"' => 'no',
+            'null' => null,   '"null"' => 'null'
+        ];
+        foreach ($data as $input => $expected)
+            $this->assertSame($expected, $parser->parse($input));
+    }
+
+    public function testParseFlatList() {
+        $parser = new MixedTypeParser();
+        // Empty list
+        $this->assertEquals([], $parser->parseList(''));
+        $this->assertEquals([], $parser->parseList('  '));
+        // Compact list
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parseList("a,b,c,d")
+        );
+        // Spaces are stripped
+        $this->assertEquals(
+            ['a', 'b', 'c'],
+            $parser->parseList("a, b,  c ")
+        );
+        // Internal spaces are preserved
+        $this->assertEquals(
+            ['a', 'b c', 'd'],
+            $parser->parseList("a, b c , d")
+        );
+        // Escaped commas are ignored
+        $this->assertEquals(
+            ['a,b', 'c'],
+            $parser->parseList("a\\,b,c")
+        );
+        // Quoted commas are ignored
+        $this->assertEquals(
+            ['a, b', 'c'],
+            $parser->parseList("'a, b', c")
+        );
+        // Trailing comma adds an empty element
+        $this->assertEquals(
+            [null, null, 'c', null],
+            $parser->parseList(",,c,")
+        );
+    }
+
+    public function testParseListValues() {
+        $parser = new MixedTypeParser();
+        $this->assertEquals([], $parser->parse("[]"));
+        $this->assertEquals([], $parser->parse("[ ]"));
+
+      //  $this->assertEquals([''], $parser->parse("['']"));
+
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parse("[a,b,c,d]")
+        );
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parse(" [a, b, c ,d] ")
+        );
+
+    }
+
+    public function testParseFlatDictionary() {
+        $parser = new MixedTypeParser();
+        // Empty object
+        $this->assertEquals([], $parser->parseDictionary(''));
+        $this->assertEquals([], $parser->parseDictionary('   '));
+        // Compact dictionary
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parseDictionary("a:1,b:2,c:3,d:4")
+        );
+        // Spaces are stripped
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2'],
+            $parser->parseDictionary(" a : 1, b: 2\n\t ")
+        );
+        // Internal spaces are preserved
+        $this->assertEquals(
+            ['a b'=>'1', 'b'=>'2 3 4'],
+            $parser->parseDictionary(" a b: 1, b: 2 3 4")
+        );
+        // Escaped delimiters are ignored
+        $this->assertEquals(
+            ['a,b'=>'1', 'b'=>'2:3,4'],
+            $parser->parseDictionary("a\\,b:1,b:2\\:3\\,4")
+        );
+        // Quoted delimiters are ignored
+        $this->assertGeneratorOutput(
+            $parser->tokenized("'a:1,b':\"23,4\""),
+            [['a:1,b',':'],['23,4',null]]
+        );
+        $this->assertEquals(
+            ['a:1,b' => '23,4'],
+            $parser->parseDictionary("'a:1,b':\"23,4\"")
+        );
+        // Trailing commas are ignored
+        $this->assertEquals(
+            ['a' => 1, 'b' => 2],
+            $parser->parseDictionary('a:1, b:2, ')
+        );
+    }
+
+    public function testParseDictValues() {
+        $parser = new MixedTypeParser();
+        $this->assertEquals([], $parser->parse("{}"));
+        $this->assertEquals([], $parser->parse("{ }"));
+
+        $this->assertEquals(['a' => null], $parser->parse("{a:}"));
+
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parse("{a:1,b:2,c:3,d:4}")
+        );
+
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parse(" { a:1, b:2, c : 3 ,d:4 } ")
+        );
+    }
+
+    public function testParseNestedStructures() {
+        $parser = new MixedTypeParser();
+        // Nested lists
+        $this->assertEquals(
+            ['a', 'b', ['ca', 'cb'], 'd'],
+            $parser->parse("[a, b, [ca, cb], d]")
+        );
+        // Dictionary nested in a list
+        $this->assertEquals(
+            ['a', 'b', ['ca'=>'0', 'cb'=>'1'], 'd'],
+            $parser->parse("[a, b, {ca:0, cb:1}, d]")
+        );
+        // List nested in a dictionary
+        $this->assertEquals(
+            ['a'=>'0', 'b'=>'1', 'c'=>['ca', 'cb'], 'd'=>'3'],
+            $parser->parse("{a:0, b:1, c:[ca, cb], d:3}")
+        );
+        // Nested dictionaries
+        $this->assertEquals(
+            ['a'=>'0', 'b'=>'1', 'c'=>['ca'=>'5', 'cb'=>'6'], 'd'=>'3'],
+            $parser->parse("{a:0, b:1, c:{ca:5, cb:6}, d:3}")
+        );
+    }
+
+    public function testRestrictedParser() {
+        $parser = new MixedTypeParser(',:  {}');
+        $this->assertEquals(
+            ['a' => '[b]', 'c' => '[d', 'e]' => ''],
+            $parser->parse('{a:[b],c:[d,e]}')
+        );
+    }
+}
\ No newline at end of file
diff --git a/extension/Tests/Unit/Core/Parser/SimpleParserTest.php b/extension/Tests/Unit/Core/Parser/SimpleParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6de69da32215b9fe1ce2860f6e97358739928f5
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/SimpleParserTest.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
+use PHPUnit\Framework\TestCase;
+
+class SimpleParserTest extends TestCase {
+
+    public function testEmptyString() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            [],
+            $parser->parse('')
+        );
+    }
+
+    public function testSingletons() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(['abcd'], $parser->parse('abcd'));
+        $this->assertEquals([42], $parser->parse('42'));
+    }
+
+    public function testSimpleList() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd','ef'],
+            $parser->parse('ab,cd,ef')
+        );
+        $parser = new SimpleParser('|');
+        $this->assertEquals(
+            ['ab','cd','ef'],
+            $parser->parse('ab|cd|ef')
+        );
+        $parser = new SimpleParser('|,.');
+        $this->assertEquals(
+            ['ab','cd','ef','gh'],
+            $parser->parse('ab|cd,ef.gh')
+        );
+    }
+
+    public function testEscapedSeparators() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd,ef'],
+            $parser->parse('ab,cd\\,ef')
+        );
+    }
+
+    public function testQuotedSeparators() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd, ef'],
+            $parser->parse('ab,"cd, ef"')
+        );
+    }
+
+    public function testIterate() {
+        $parser = new SimpleParser(',');
+        $iterator = $parser->iterate('a,b,c');
+        $expected = ['a', 'b', 'c'];
+        foreach($iterator as $value) {
+            $this->assertSame(current($expected), $value);
+            next($expected);
+        }
+        $this->assertFalse(current($expected));
+
+    }
+}
+
diff --git a/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php b/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cdf586cf755c735473f9f1a9a2b889a70db83653
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\StringTokenizer;
+use PHPUnit\Framework\TestCase;
+
+class StringTokenizerTest extends TestCase {
+
+    protected function assertTokenizer($delimiters, $data, $expected) {
+        $parser = new StringTokenizer($delimiters);
+        $generator = $parser->tokenized($data);
+        foreach ($expected as $data) {
+            list($token, $delimiter) = $generator->current();
+            if (is_array($data)) {
+                $this->assertSame($data[0], $token->value, "Expecting $data[0]");
+                $this->assertSame($data[1], $delimiter, "Expecting '$data[1]'");
+            } else {
+                $this->assertSame($data, $token->value, "Expecting $data");
+            }
+            $generator->next();
+        }
+        $this->assertNull($generator->current());
+    }
+
+    public function testOffset() {
+        $parser = new StringTokenizer(':|');
+        $input = 'a:bc:de|f';
+        $offsets = [1,4,7,null];
+        $tokens = $parser->tokenized($input);
+        foreach($offsets as $value) {
+            $tokens->current();
+            if (!is_null($value)) $this->assertSame($value, $parser->offset());
+            $tokens->next();
+        }
+        $this->assertFalse($tokens->valid());
+    }
+
+    public function testEmptyString() {
+        $this->assertTokenizer(':,[]{}', '', []);
+    }
+
+    public function testSimpleString() {
+        $this->assertTokenizer(':,[]{}', 'abc', [['abc',null]]);
+    }
+
+    public function testUnescapedDelimiters() {
+        $this->assertTokenizer(
+            ':,./]{',
+            "x:7,y.z/24]{x",
+            [[ 'x', ':'], ['7', ','], ['y', '.'], ['z', '/'],
+                ['24', ']'], ['','{'], ['x',null]]
+        );
+    }
+
+    public function testEscapedDelimiters() {
+        $this->assertTokenizer(
+            ':,./]{',
+            "x\\:7\\,y\\.z\\/24\\]\\{x",
+            [[ "x:7,y.z/24]{x", null]]
+        );
+    }
+
+    public function testEscapedNonDelimiters() {
+        $this->assertTokenizer(
+            ':',
+            'x\\\\y\\n',
+            ['x\\\\y\\n']
+        );
+    }
+
+    public function testTokensAreTrimmed() {
+        $this->assertTokenizer(
+            '|',
+            ' a|b | cd ',
+            ['a', 'b', 'cd']
+        );
+    }
+
+    public function testSpacesInside() {
+        $this->assertTokenizer(
+            ':',
+            "x y: sy   ca : f\t\ng",
+            ['x y', 'sy   ca', "f\t\ng"]
+        );
+    }
+
+    public function testQuotesAreRemoved() {
+        $this->assertTokenizer(
+            ':',
+            '"x y":ab\'x\'cd',
+            ['x y', 'abxcd']
+        );
+    }
+
+    public function testQuotedSpacesArePreserved() {
+        $this->assertTokenizer(
+            ':',
+            '" "x:ab" x ":\'  \'',
+            [' x', 'ab x ', '  ']
+        );
+    }
+
+    public function testEscapedQuotesArePreserved() {
+        $this->assertTokenizer(
+            ':',
+            '\"x y\":ab\\\'cd',
+            ['"x y"', "ab'cd"]
+        );
+    }
+
+    public function testNestedQuotesAreNotParsed() {
+        $this->assertTokenizer(
+            ':',
+            '"\'":\'"\'',
+            ["'", '"']
+        );
+    }
+
+    public function testQuotedDelimitersAreIgnored() {
+        $this->assertTokenizer(
+            ':,|',
+            'x:a\\|b|c\\,d\\:e:24',
+            ['x', 'a|b', 'c,d:e', '24']
+        );
+    }
+}
diff --git a/extension/Tests/Unit/Core/Report/ReportTest.php b/extension/Tests/Unit/Core/Report/ReportTest.php
index 789b04a23d134f5480adf434c6ab768b05d85644..b8135a028bc918d113e21ada43283591505233c9 100644
--- a/extension/Tests/Unit/Core/Report/ReportTest.php
+++ b/extension/Tests/Unit/Core/Report/ReportTest.php
@@ -1384,6 +1384,9 @@ EOF;
      */
     public function testReportPageWrapper() {
 
+        // Report notation 'alias' with alias
+        $this->store->setVar(TOKEN_ALIAS . '.1', 'myAlias', STORE_TYPO3);
+
         $line = <<<EOF
 10.sql = SELECT firstname FROM Person ORDER BY id LIMIT 2
 10.head = <table>
@@ -1403,6 +1406,32 @@ EOF;
 10.10.tail = Dynamic tail
 10.10.althead = No record found
 10.10.altsql = SELECT 'alt sql fired'
+EOF;
+
+        $result = $this->report->process($line);
+        $expect = "<table><tr><td>John</td><br>Static headDynamic headnestedDynamic tailStatic tail</tr>--<tr><td>Jane</td><br>Static headNo record foundalt sql firedStatic tail</tr></table>";
+        $this->assertEquals($expect, $result);
+
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT firstname FROM Person ORDER BY id LIMIT 2
+1.head = <table>
+1.tail = </table>
+1.rbeg = <tr>
+1.rend = <br>
+1.renr = </tr>
+1.fbeg = <td>
+1.fend = </td>
+1.rsep = --
+1.fsep = ++
+
+1.2.sql = SELECT 'nested' FROM (SELECT '') AS fake WHERE '{{myAlias.line.count}}'='1'
+1.2.shead = Static head
+1.2.stail = Static tail
+1.2.head = Dynamic head
+1.2.tail = Dynamic tail
+1.2.althead = No record found
+1.2.altsql = SELECT 'alt sql fired'
 EOF;
 
         $result = $this->report->process($line);
@@ -1425,6 +1454,9 @@ EOF;
      */
     public function testReportContent() {
 
+        // Report notation 'alias' with alias
+        $this->store->setVar(TOKEN_ALIAS . '.1', 'myAlias', STORE_TYPO3);
+
         $line = <<<EOF
 10.sql = SELECT 'Hello'
 20.sql = SELECT 'World'
@@ -1434,6 +1466,16 @@ EOF;
         $expect = "HelloWorld{{10.line.content}}";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT 'Hello'
+2.sql = SELECT 'World'
+2.tail = {{myAlias.line.content}}
+EOF;
+        $result = $this->report->process($line);
+        $expect = "HelloWorld{{myAlias.line.content}}";
+        $this->assertEquals($expect, $result);
+
         $line = <<<EOF
 10.sql = SELECT 'Hello'
 10.content = hide
@@ -1444,6 +1486,17 @@ EOF;
         $expect = "WorldHello";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT 'Hello'
+1.content = hide
+2.sql = SELECT 'World'
+2.tail = {{myAlias.line.content}} 
+EOF;
+        $result = $this->report->process($line);
+        $expect = "WorldHello";
+        $this->assertEquals($expect, $result);
+
         $line = <<<EOF
 10.sql = SELECT 'Hello'
 10.content = show
@@ -1454,6 +1507,17 @@ EOF;
         $expect = "HelloWorldHello";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT 'Hello'
+1.content = show
+2.sql = SELECT 'World'
+2.tail = {{myAlias.line.content}} 
+EOF;
+        $result = $this->report->process($line);
+        $expect = "HelloWorldHello";
+        $this->assertEquals($expect, $result);
+
         // Check that current row can be reused in head, tail, rbeg, rend, renr
         $line = <<<EOF
 10.sql = SELECT 'Hello'
@@ -1468,11 +1532,37 @@ EOF;
         $expect = "HelloHelloHelloHelloHelloHello";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        // Check that current row can be reused in head, tail, rbeg, rend, renr
+        $line = <<<EOF
+1.sql = SELECT 'Hello'
+1.content = show
+1.head = {{myAlias.line.content}}
+1.tail = {{myAlias.line.content}}
+1.rbeg = {{myAlias.line.content}}
+1.rend = {{myAlias.line.content}}
+1.renr = {{myAlias.line.content}}
+EOF;
+        $result = $this->report->process($line);
+        $expect = "HelloHelloHelloHelloHelloHello";
+        $this->assertEquals($expect, $result);
+
         // Check single tick escape
         $line = <<<EOF
 10.sql = SELECT "Hel'lo"
 10.content = hide
 20.sql = SELECT '--{{10.line.content::s}}--', "--{{10.line.content}}--", "--{{10.line.content::s}}--"
+EOF;
+        $result = $this->report->process($line);
+        $expect = "--Hel'lo----Hel'lo----Hel'lo--";
+        $this->assertEquals($expect, $result);
+
+        // Report notation 'alias' with alias
+        // Check single tick escape
+        $line = <<<EOF
+1.sql = SELECT "Hel'lo"
+1.content = hide
+2.sql = SELECT '--{{myAlias.line.content::s}}--', "--{{myAlias.line.content}}--", "--{{myAlias.line.content::s}}--"
 EOF;
         $result = $this->report->process($line);
         $expect = "--Hel'lo----Hel'lo----Hel'lo--";
@@ -1563,15 +1653,27 @@ EOF;
      */
     public function testReportVariables() {
 
+        // Report notation 'alias' with alias
+        $this->store->setVar(TOKEN_ALIAS . '.1', 'myAlias', STORE_TYPO3);
+        $this->store->setVar(TOKEN_ALIAS . '.1.2', 'myAlias2', STORE_TYPO3);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1");
         $this->assertEquals("normal text ", $result);
 
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.hidden}}'");
         $this->assertEquals("normal text hidden", $result);
 
+        // Report notation 'alias' with alias
+        $result = $this->report->process("1.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n1.2.sql = SELECT '{{myAlias.hidden}}'");
+        $this->assertEquals("normal text hidden", $result);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.unknown}}'");
         $this->assertEquals("normal text {{10.unknown}}", $result);
 
+        // Report notation 'alias' with alias
+        $result = $this->report->process("1.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n1.2.sql = SELECT '{{myAlias.unknown}}'");
+        $this->assertEquals("normal text {{myAlias.unknown}}", $result);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{fake}}'");
         $this->assertEquals("normal text {{fake}}", $result);
 
@@ -1588,12 +1690,24 @@ EOF;
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.line.insertId}} '");
         $this->assertEquals("normal text hello world -1-2-0 normal text hello world -2-2-0 ", $result);
 
+        // Report notation 'alias' with alias
+        $result = $this->report->process("1.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n1.2.sql = SELECT '{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}} '");
+        $this->assertEquals("normal text hello world -1-2-0 normal text hello world -2-2-0 ", $result);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}} '");
         $this->assertEquals("normal text hello world -1-2 normal text hello world -2-2 ", $result);
 
+        // Report notation 'alias' with alias
+        $result = $this->report->process("1.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n1.2.sql = SELECT '{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}} '");
+        $this->assertEquals("normal text hello world -1-2 normal text hello world -2-2 ", $result);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V}}-{{10.line.count}}-{{10.line.total}}-{{10.10.line.count}}-{{10.10.line.total}} '");
         $this->assertEquals("normal text hello world -1-2-1-1 normal text hello world -2-2-1-1 ", $result);
 
+        // Report notation 'alias' with alias
+        $result = $this->report->process("1.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n1.2.sql = SELECT '{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias2.line.count}}-{{myAlias2.line.total}} '");
+        $this->assertEquals("normal text hello world -1-2-1-1 normal text hello world -2-2-1-1 ", $result);
+
         $result = $this->report->process("10.sql = SELECT 'normal ', 'hidden' AS _hidden, 'text ' FROM Person ORDER BY id\n10.10.sql = SELECT '{{fake:V:::not found}} '");
         $this->assertEquals("normal text hello world  normal text hello world  ", $result);
 
@@ -1643,6 +1757,26 @@ EOF;
         $expect = "h:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0,Doe-fe:hello world-1-2-0,fs:hello world-1-2-0,-fb:hello world-1-2-0,John-fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,rs:hello world-1-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0,Smith-fe:hello world-2-2-0,fs:hello world-2-2-0,-fb:hello world-2-2-0,Jane-fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2
+1.head = h:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.tail = t:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.rbeg = rb:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.rend = re:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.renr = rr:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.fbeg = -fb:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.fend = -fe:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.rsep = rs:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.fsep = fs:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+EOF;
+
+        $this->store->setVar('fake', 'hello world', STORE_VAR);
+        $this->store->setVar(TOKEN_ALIAS . '.1', 'myAlias', STORE_TYPO3);
+        $result = $this->report->process($line);
+        $expect = "h:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0,Doe-fe:hello world-1-2-0,fs:hello world-1-2-0,-fb:hello world-1-2-0,John-fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,rs:hello world-1-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0,Smith-fe:hello world-2-2-0,fs:hello world-2-2-0,-fb:hello world-2-2-0,Jane-fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,";
+        $this->assertEquals($expect, $result);
+
         $line = <<<EOF
 10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2
 10.10.sql = SELECT ' blue '
@@ -1662,6 +1796,26 @@ EOF;
         $expect = "DoeJohnh:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0, blue -fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,t:hello world-1-2-0,SmithJaneh:hello world-2-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0, blue -fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,";
         $this->assertEquals($expect, $result);
 
+        // Report notation 'alias' with alias
+        $line = <<<EOF
+1.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2
+1.2.sql = SELECT ' blue '
+1.2.head = h:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.tail = t:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.rbeg = rb:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.rend = re:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.renr = rr:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.fbeg = -fb:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.fend = -fe:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.rsep = rs:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+1.2.fsep = fs:{{fake:V}}-{{myAlias.line.count}}-{{myAlias.line.total}}-{{myAlias.line.insertId}},
+EOF;
+
+        $this->store->setVar('fake', 'hello world', STORE_VAR);
+        $this->store->setVar(TOKEN_ALIAS . '.1', 'myAlias', STORE_TYPO3);
+        $result = $this->report->process($line);
+        $expect = "DoeJohnh:hello world-1-2-0,rb:hello world-1-2-0,-fb:hello world-1-2-0, blue -fe:hello world-1-2-0,re:hello world-1-2-0,rr:hello world-1-2-0,t:hello world-1-2-0,SmithJaneh:hello world-2-2-0,rb:hello world-2-2-0,-fb:hello world-2-2-0, blue -fe:hello world-2-2-0,re:hello world-2-2-0,rr:hello world-2-2-0,t:hello world-2-2-0,";
+        $this->assertEquals($expect, $result);
     }
 
 
diff --git a/extension/Tests/Unit/Core/Store/StoreTest.php b/extension/Tests/Unit/Core/Store/StoreTest.php
index 449738d1ce62b92c38529e0215f9cdb79de54682..c50ddf77a148896829fce635faf577fd535483bd 100644
--- a/extension/Tests/Unit/Core/Store/StoreTest.php
+++ b/extension/Tests/Unit/Core/Store/StoreTest.php
@@ -439,7 +439,6 @@ class StoreTest extends TestCase {
 //            SYSTEM_DO_NOT_LOG_COLUMN => SYSTEM_DO_NOT_LOG_COLUMN_DEFAULT,
             SYSTEM_PROTECTED_FOLDER_CHECK => 1,
             SYSTEM_CMD_WGET => 'wget >/dev/null 2>&1'
-
         ];
 
         $body = json_encode([
diff --git a/extension/composer.json b/extension/composer.json
index 384d4a569451317cc1ed83089f13ce90b4708b9f..8824686e2ad8f659fff4475fb59276db055c2dfc 100644
--- a/extension/composer.json
+++ b/extension/composer.json
@@ -3,7 +3,8 @@
     "phpoffice/phpspreadsheet": "^1.3",
     "ext-json": "*",
     "twig/twig": "^2.0",
-    "ezyang/htmlpurifier": "^4.15"
+    "ezyang/htmlpurifier": "^4.15",
+    "firebase/php-jwt": "^6.9"
   },
   "require-dev": {
     "phpunit/phpunit": "^9"
diff --git a/extension/ext_conf_template.txt b/extension/ext_conf_template.txt
index a0e4908e21f6dccd33aa3062927fa1efce5e712b..677b072fd45c842303994dbe66a4cdc2ea782135 100644
--- a/extension/ext_conf_template.txt
+++ b/extension/ext_conf_template.txt
@@ -133,7 +133,6 @@ encryptionMethod =
 protectedFolderCheck = 1
 
 
-
 # cat=form-config/config; type=string; label=Dirty record lock timeout (seconds):Default is '900'. Time in seconds to lock a record, starting from the first modification. If lock expires, it is acquired again on the next modification.
 recordLockTimeoutSeconds = 900
 
diff --git a/javascript/src/BSTabs.js b/javascript/src/BSTabs.js
index 7aef8f3a3175018046d4a58ba5e7246ac6d3333e..347754af0ff2534f6e7fcd92785483d4202b7295 100644
--- a/javascript/src/BSTabs.js
+++ b/javascript/src/BSTabs.js
@@ -37,7 +37,7 @@ var QfqNS = QfqNS || {};
         this.eventEmitter = new EventEmitter();
         this.currentFormName = $('#' + this.tabId + ' .active a[data-toggle="tab"]')[0].hash.slice(1).split("_")[0];
         this.currentRecordId = $('#' + this.tabId + ' a[data-toggle="tab"]')[0].id.split("-")[2];
-        this.currentActiveLastPill = document.getElementById('qfqTabs').getAttribute('data-active-last-pill');
+        this.currentActiveLastPill = document.getElementById(this.tabId).getAttribute('data-active-last-pill');
 
         // Fill this.tabs
         this.fillTabInformation();
diff --git a/javascript/src/Element/FormGroup.js b/javascript/src/Element/FormGroup.js
index 3ea9086fc3ce286f18cc1112be99c5f7b5669689..7d707635e76093717934139a3555e683d3ce72c5 100644
--- a/javascript/src/Element/FormGroup.js
+++ b/javascript/src/Element/FormGroup.js
@@ -92,14 +92,21 @@ QfqNS.Element = QfqNS.Element || {};
      * @private
      */
     n.FormGroup.prototype.$findFormGroup = function ($enclosedElement) {
-        var $formGroup = $('#' + $enclosedElement.attr('id') + '-i');
+        var idArray = $enclosedElement.attr('id').split("-");
+        var searchString = "#";
+        for(var i = 0; i < 8 && i < idArray.length; i++) {
+            searchString += idArray[i] + "-";
+        }
+        var $formGroup = $(searchString + 'i');
         
         if (!$formGroup || $formGroup.length === 0) {
-            throw new Error("Unable to find Form Group");
+            console.log("Unable to find Form Group for", $enclosedElement);
+            console.log("trying with: " + '#' + $enclosedElement.attr('id') + '-i');
+            throw new Error("Unable to find Form Group for", $enclosedElement);
         }
 
         if ($formGroup.length > 1) {
-            $formGroup = $('#' + $enclosedElement.attr('id') + '-i');
+            $formGroup = $(searchString + 'i');
             console.log("Enclosed Element Id: " + $enclosedElement.attr('id'));
             if ($formGroup.length !== 1) {
                 throw new Error("enclosed element yields ambiguous form group");
diff --git a/javascript/src/Element/NameSpaceFunctions.js b/javascript/src/Element/NameSpaceFunctions.js
index e30998c3fee2087c1bc6aa42e96250edbf096d87..c19cd8bbaff44da7f10b0b982e8e25c0bb1a56c6 100644
--- a/javascript/src/Element/NameSpaceFunctions.js
+++ b/javascript/src/Element/NameSpaceFunctions.js
@@ -68,6 +68,7 @@ QfqNS.Element = QfqNS.Element || {};
             case 'checkbox':
                 return new n.Checkbox($element);
             case 'nonFormGroupCheckbox':
+            case "file":
                 return $element;
             case 'radio':
                 return new n.Radio($element);
diff --git a/javascript/src/Form.js b/javascript/src/Form.js
index 19958e805b3efa4104053b407e55f19824f41d07..ba2a1a1f1ab864a23dad9e51b747c544558f6054 100644
--- a/javascript/src/Form.js
+++ b/javascript/src/Form.js
@@ -91,7 +91,7 @@ var QfqNS = QfqNS || {};
         document.addEventListener('keydown', function(event) {
             if (event.ctrlKey && event.altKey && event.key === 's') {
                 console.log("submit");
-                $("#save-button:not([disabled=disabled])").click();
+                $("#save-button-" + this.formId + ":not([disabled=disabled])").click();
             }
         });
 
@@ -181,6 +181,7 @@ var QfqNS = QfqNS || {};
         // Reset disabled inputs
         disabled.attr('disabled','disabled');
 
+        console.log("Serialized form", serializedForm);
         $.post(submitUrl, serializedForm)
             .done(this.ajaxSuccessHandler.bind(this))
             .fail(this.submitFailureHandler.bind(this));
@@ -230,8 +231,7 @@ var QfqNS = QfqNS || {};
                 textStatus: textStatus,
                 jqXHR: jqXHR
             }));
-        $("#save-button").removeClass('btn-warning active disabled');
-        $("#save-button").addClass('btn-default');
+        
         this.saveInProgress = false;
     };
 
diff --git a/javascript/src/Helper/tinyMCE.js b/javascript/src/Helper/tinyMCE.js
index 095bbbe5c4f9e6e69831e3fa6a23f33bec2258d2..18120be577c7e8e10014fd313c0cf1d13c30d2be 100644
--- a/javascript/src/Helper/tinyMCE.js
+++ b/javascript/src/Helper/tinyMCE.js
@@ -148,7 +148,14 @@ QfqNS.Helper = QfqNS.Helper || {};
                     });
                 };
 
-                tinymce.init(config);
+                var defaults = {
+                    relative_urls : false,
+                    remove_script_host : false,
+                };
+
+                var tinyConfig = Object.assign(defaults, config);
+
+                tinymce.init(tinyConfig);
                 if($(this).is('[disabled]')) {
                     myEditor.setMode("readonly");
                 }
diff --git a/javascript/src/Main.js b/javascript/src/Main.js
index b4660c404f6f6d4d28c34e28fbb58032a5870c49..5fef921a534b7aefe762c2e82103402ad0e8bdce 100644
--- a/javascript/src/Main.js
+++ b/javascript/src/Main.js
@@ -28,10 +28,20 @@ $(document).ready( function () {
         $('button.qfq-column-selector').click(function () {
             $('.tablesorter-column-selector>label>input').addClass('qfq-skip-dirty');
         });
+
+
+        var collection = document.getElementsByClassName("qfq-form");
+        console.log(collection);
+        var qfqPages = [];
+        for (const form of collection) {
+            const page = new n.QfqPage(form.dataset);
+            qfqPages.push(page);
+        }
+
     } catch (e) {
         console.log(e);
     }
-
+    
         $('.qfq-auto-grow').each(function() {
             var minHeight = $(this).attr("rows") * 14 + 18;
             var newHeight = $(this).prop('scrollHeight');
@@ -194,6 +204,7 @@ $(document).ready( function () {
         };
 
         n.initializeQfqClearMe();
+        n.initializeDatetimepicker();
         n.Helper.calendar();
 
     })(QfqNS);
diff --git a/javascript/src/Plugins/qfq.fabric.js b/javascript/src/Plugins/qfq.fabric.js
index f87b882f1c1033c0eb479213fd99671bcfeba6f8..efc5ffa087f5330ce4e3b8332a27d8d1fa57f4d4 100644
--- a/javascript/src/Plugins/qfq.fabric.js
+++ b/javascript/src/Plugins/qfq.fabric.js
@@ -181,13 +181,13 @@ $(function (n) {
         };
 
         ModeSettings.prototype.getButtonById = function (needle) {
-            var needleInHaystack = {};
+            var needleInHaystack = false;
             this.myButtons.forEach(function (haystack) {
                 if (haystack[0].id === needle) {
                     needleInHaystack = haystack;
                 }
             });
-            if (needleInHaystack === {}) {
+            if (needleInHaystack) {
                 console.error("Button not found, id: " + string);
             } else {
                 return needleInHaystack;
diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js
index c2a066fa9dd508f0d3f0dfb56ac51283675885ff..23f56239bb34cbcfb134b54ce16bd1fd03b0e90b 100644
--- a/javascript/src/QfqForm.js
+++ b/javascript/src/QfqForm.js
@@ -146,9 +146,9 @@ var QfqNS = QfqNS || {};
         this.applyFormConfiguration(configurationData);
 
         // Initialize jqxDateTimeInput elements.
-        n.Helper.jqxDateTimeInput();
+        //n.Helper.jqxDateTimeInput();
         // Initialize jqxComboBox elements.
-        n.Helper.jqxComboBox();
+        //n.Helper.jqxComboBox();
         // Deprecated
         //n.Helper.jqxEditor();
         n.Helper.tinyMce();
@@ -213,7 +213,7 @@ var QfqNS = QfqNS || {};
         }];
         if (obj.data.status == "conflict_allow_force") {
             messageButtons.push({
-                label: "Ignore",
+                label: "Continue",
                 eventName: 'ignore'
             });
         }
@@ -278,10 +278,14 @@ var QfqNS = QfqNS || {};
                 break;
             case "conflict_allow_force":
                 messageType = "warning";
-                messageButtons.push({
-                    label: "Ignore",
+
+                messageButtons = [{
+                    label: "Continue",
                     eventName: 'ignore'
-                });
+                }, {
+                    label: "Cancel",
+                    eventName: 'reload'
+                }];
                 break;
             case "error":
                 messageType = "error";
@@ -644,6 +648,7 @@ var QfqNS = QfqNS || {};
 
     n.QfqForm.prototype.submit = function (queryParameters) {
         var submitQueryParameters;
+        console.log("Save in progress", submitQueryParameters);
         var alert;
         var submitReason;
 
@@ -658,7 +663,7 @@ var QfqNS = QfqNS || {};
                 }
 
 
-                var form = document.getElementById(this.form.formId);
+                var form = document.getElementById(this.formId);
                 var inputs = form.elements;
 
                 for (var i = 0; i < inputs.length; i++) {
@@ -710,6 +715,7 @@ var QfqNS = QfqNS || {};
 
         submitQueryParameters = $.extend({}, queryParameters, submitReason);
         this.form.submitTo(this.submitTo, submitQueryParameters);
+        console.log("Submitting with", submitQueryParameters);
         this.form.saveInProgress = true;
     };
 
@@ -953,7 +959,7 @@ var QfqNS = QfqNS || {};
      * @private
      */
     n.QfqForm.prototype.getSaveButton = function () {
-        return $("#save-button");
+        return $("#save-button-" + this.formId);
     };
 
     /**
@@ -963,7 +969,7 @@ var QfqNS = QfqNS || {};
      * @private
      */
     n.QfqForm.prototype.getCloseButton = function () {
-        return $("#close-button");
+        return $("#close-button-" + this.formId);
     };
 
     /**
@@ -973,7 +979,7 @@ var QfqNS = QfqNS || {};
      * @private
      */
     n.QfqForm.prototype.getDeleteButton = function () {
-        return $("#delete-button");
+        return $("#delete-button-" + this.formId);
     };
 
     /**
@@ -983,7 +989,7 @@ var QfqNS = QfqNS || {};
      * @private
      */
     n.QfqForm.prototype.getNewButton = function () {
-        return $("#form-new-button");
+        return $("#form-new-button-" + this.formId);
     };
 
 
@@ -1108,6 +1114,8 @@ var QfqNS = QfqNS || {};
      */
     n.QfqForm.prototype.handleSubmitSuccess = function (form, data) {
         n.Log.debug('Reset form state');
+        this.getSaveButton().removeClass('btn-warning active disabled');
+        this.getSaveButton().addClass('btn-default');
         form.resetFormChanged();
         this.resetLockState();
 
@@ -1272,9 +1280,8 @@ var QfqNS = QfqNS || {};
          * confusing.
          */
         var $formGroup = this.getFormGroupByControlName(formControlName);
-        if (!$formGroup) {
-            return;
-        }
+        if (!$formGroup) return;
+
         var $helpBlockColumn;
         var $formGroupSubDivs = $formGroup.find("div");
         if ($formGroupSubDivs.length < 3) {
diff --git a/javascript/src/QfqPage.js b/javascript/src/QfqPage.js
index 02791409a413d0762d60b746eafd5e74133181e4..0b9dae588e107cbf2610f6c93218f937bb087e0d 100644
--- a/javascript/src/QfqPage.js
+++ b/javascript/src/QfqPage.js
@@ -24,6 +24,7 @@ var QfqNS = QfqNS || {};
      * @name QfqNS.QfqPage
      */
     n.QfqPage = function (settings) {
+        console.log("Creating QFQPage", settings);
         this.qfqForm = {};
         this.settings = $.extend(
             {
@@ -40,6 +41,8 @@ var QfqNS = QfqNS || {};
             }, settings
         );
 
+        n.Log.level = settings.logLevel;
+
         this.intentionalClose = false;
 
         try {
@@ -117,6 +120,7 @@ var QfqNS = QfqNS || {};
                     that.qfqForm.releaseLock(true);
                 };
             })(this));
+            this.recordList = new n.QfqRecordList(settings.apiDeleteUrl);
         } catch (e) {
             n.Log.error(e.message);
             this.qfqForm = null;
@@ -125,7 +129,7 @@ var QfqNS = QfqNS || {};
         var page = this;
         // Initialize Fabric to access form events
         try {
-            $(".annotate-graphic").each(function() {
+            $("#" + this.formId + " .annotate-graphic").each(function() {
                 var qfqFabric = new QfqNS.Fabric();
                 qfqFabric.initialize($(this), page);
             });
@@ -134,7 +138,7 @@ var QfqNS = QfqNS || {};
         }
 
         try {
-            $(".annotate-text").each(function() {
+            $("#" + this.formId + " .annotate-text").each(function() {
                 var codeCorrection = new QfqNS.CodeCorrection();
                 codeCorrection.initialize($(this), page);
             });
@@ -144,7 +148,7 @@ var QfqNS = QfqNS || {};
 
         QfqNS.TypeAhead.install(this.settings.typeAheadUrl);
         QfqNS.CharacterCount.initialize();
-        n.initializeDatetimepicker(false);
+        //n.initializeDatetimepicker(false);
     };
 
     /**
diff --git a/javascript/src/QfqRecordList.js b/javascript/src/QfqRecordList.js
index 364b19537a9ebeb8b0ee50bdaee23e13507e7f93..15c9853466604a5bc6539149e52b8cd112fd8a88 100644
--- a/javascript/src/QfqRecordList.js
+++ b/javascript/src/QfqRecordList.js
@@ -22,6 +22,7 @@ var QfqNS = QfqNS || {};
      * @name QfqNS.QfqRecordList
      */
     n.QfqRecordList = function (deleteUrl) {
+        console.log("initialized with this url", deleteUrl);
         this.deleteUrl = deleteUrl;
         this.deleteButtonClass = 'record-delete';
         this.recordClass = 'record';
diff --git a/package.json b/package.json
index 27a62141a9c00a87574c1e8500294efb2ba53b9c..a1a8a5676f48fa2458b340a82f41d0cf3c8af4a9 100644
--- a/package.json
+++ b/package.json
@@ -3,12 +3,11 @@
   "version": "1.0.0",
   "dependencies": {
     "@fortawesome/fontawesome-free": "^5.15.3",
-    "bootlint": "^0.14.2",
     "bootstrap": "^3.3.6",
     "bootstrap-datetimepicker": "0.0.7",
     "bootstrap-validator": "^0.11.5",
     "chart.js": "^2.9.4",
-    "codemirror": "^5.65.12",
+    "codemirror": "^5.65.15",
     "corejs-typeahead": "^1.3.1",
     "eonasdan-bootstrap-datetimepicker": "^4.17.49",
     "fullcalendar": "^3.10.2",
@@ -19,25 +18,25 @@
     "grunt-contrib-jasmine": "^1.1.0",
     "grunt-contrib-jshint": "^1.1.0",
     "grunt-contrib-less": "^1.2.0",
-    "grunt-contrib-uglify": "^2.2.0",
     "grunt-contrib-watch": "^1.0.0",
-    "http-server": "^14.1.1",
+    "grunt-terser": "^2.0.0",
     "jquery": "latest",
     "jqwidgets-framework": "4.2.1",
-    "jsdoc": "^3.6.11",
-    "mocha": "^3.2.0",
     "moment": "^2.29.4",
     "popper.js": "^1.16.1",
-    "selenium-webdriver": "^3.3.0",
+    "selenium-webdriver": "^4.14.0",
     "should": "^11.2.1",
     "tablesorter": "^2.31.3",
+    "terser": "^5.21.0",
     "tinymce": "^4.9.11",
     "wolfy87-eventemitter": "^4.3.0"
   },
-  "devDependencies": {},
   "scripts": {
     "test": "mocha tests/selenium/test*.js"
   },
   "license": "ISC",
-  "repository": "https://git.math.uzh.ch/typo3/qfq"
+  "repository": "https://git.math.uzh.ch/typo3/qfq",
+  "devDependencies": {
+    "mocha": "^10.2.0"
+  }
 }