diff --git a/.gitignore b/.gitignore
index 8b093469de16a3ff5b64c9f6ea98afcbe51247e0..a582212e0689392793bb1889e7e27a221769b68c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
 .phpunit.result.cache
 nbprojec
 nohup.out
+.DS_Store
 
 # Created by .ignore support plugin (hsz.mobi)
 .python_virtualenv/
@@ -71,6 +72,8 @@ composer.lock
 
 /javascript/src/.vscode
 /javascript/src/npm-debug.log
+/javascript/build/dist
+/less/dist
 
 /docker/chromedriver
 /docker/geckodriver
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a8be0e21a3d3c10f537ea57a595e820867dea4e8..6bc7ffb4ac638c7896dbd5b9ed7098500ecc7fed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,7 +11,8 @@ variables:
 stages:
   - before
   - build
-#  - selenium
+  - test
+  #  - selenium
 
 #documentation:
 #  stage: before
@@ -29,7 +30,7 @@ snapshot:
     paths:
       - build/
   script:
-    - make VERSION=${VERSION} phpunit_snapshot
+    - make VERSION=${VERSION} snapshot
     - chmod a+r qfq_${VERSION}_*.zip
     - echo "mv qfq_${VERSION}_*.zip qfq_${VERSION}_${RELDATE}-${CI_COMMIT_REF_NAME}.zip"
     - mv qfq_${VERSION}_*.zip qfq_${VERSION}_${RELDATE}-${CI_COMMIT_REF_NAME}.zip
@@ -45,11 +46,17 @@ release:
     paths:
       - build/
   script:
-    - make VERSION=${VERSION} phpunit_release
+    - make VERSION=${VERSION} release
     - chmod a+r qfq_${VERSION}_*.zip
     - scp qfq_${VERSION}_*.zip w16:qfq/releases/
     - mv qfq_${VERSION}_*.zip build/qfq.zip
 
+tests:
+  stage: test
+  script:
+    - make phpunit
+  
+
   #selenium:
   #  stage: selenium
   #  script:
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 23f25081d7dbe32da48a1df36b7b4a450914d4b2..677d91d975dc1ef3710fc0a92d58b9169529c179 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
 ^^^^^^^^
@@ -3158,30 +3168,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 mins 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 e4cf107554758fb7c7d76377d573c7b6858d5d73..74ce355f6fb7c0863fe858f51f34db6ac3c785b7 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/Makefile b/Makefile
index af23e110dc44860f4ea30f1f9ecc97f955a88046..86c102eec88d07024354f6d5ef3b942ef27b10b0 100644
--- a/Makefile
+++ b/Makefile
@@ -59,14 +59,15 @@ plantuml:
 	cd doc/diagram ; $(MAKE)
 
 bootstrap: .npmpackages .plantuml_install .virtual_env
-	npm update
-	grunt default
+	npm install
+	npm run build
 	# take care that phpOffice is located under 'qfq/Resources/Private/vendor/phpoffice'
 	# cd extension/Resources/Private; composer update
 	cd extension; composer update
 
 basic: .npmpackages .virtual_env
-	grunt default
+	npm install
+	npm run build
 	# IMPORTANT: install composer with no-dev flag for deployment!
 	cd extension; composer install --no-dev --optimize-autoloader; cd vendor/phpoffice/phpspreadsheet; rm -rf .github bin docs samples .g* .s* .t* C* c* m* p*
 
@@ -81,7 +82,6 @@ basic: .npmpackages .virtual_env
 	node --version
 	which node
 	echo "${PATH}"
-	npm ls -g grunt-cli 2>/dev/null || { echo "Please install grunt-cli npm package using 'npm install -g grunt-cli'" 1>&2 ; exit 1; }
 	# update npm at persistent location and copy node_modules (to speed up process)
 	mkdir -p $(VAR_TMP)/npm
 	/bin/cp package.json $(VAR_TMP)/npm/
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/Constants.php b/extension/Classes/Core/Constants.php
index 30d1bc71d847793010e25b2f765ff44ada36297f..b1f928076006df7c8b94a5578c3823612089ec87 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -254,6 +254,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;
@@ -523,6 +524,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;
@@ -769,6 +771,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#';
@@ -1668,6 +1671,7 @@ const COLUMN_UID = 'uid';
 const COLUMN_HEADER = 'header';
 const COLUMN_TITLE = 'title';
 const COLUMN_SUBHEADER = 'subheader';
+const COLUMN_EXPIRE = 'expire';
 const COLUMN_UPLOAD_ID = 'uploadId';
 
 const INDEX_PHP = 'index.php';
@@ -1776,6 +1780,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';
 
@@ -1864,6 +1871,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';
@@ -2215,6 +2224,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/HelperFile.php b/extension/Classes/Core/Helper/HelperFile.php
index b709f90ea2a7d26cf7574bff9da92811720add6d..2c27f5da010f883d254f1efae787edca0f4eb7b3 100644
--- a/extension/Classes/Core/Helper/HelperFile.php
+++ b/extension/Classes/Core/Helper/HelperFile.php
@@ -589,9 +589,9 @@ class HelperFile {
      * @param $content
      * @throws \UserFormException
      */
-    public static function file_put_contents($pathFileName, $content) // : void
+    public static function file_put_contents($pathFileName, $content, $flag = 0) // : void
     {
-        $success = file_put_contents($pathFileName, $content);
+        $success = file_put_contents($pathFileName, $content, $flag);
         if ($success === false) {
             throw new \UserFormException(json_encode([
                 ERROR_MESSAGE_TO_USER => "Writing file failed.",
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..88a5eb8c57016c82f59a5b5b758306607940d609
--- /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..f92e6128a664ec9853a5d6e03aca3483edca027d
--- /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..157cfad3532730b551f482c042b6cb5da608f040
--- /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..5e94c180a685ebd27bc4002de4e45ce353153ad1
--- /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..b33c71509fc6ffd6fb1a90713a165a5082e22d49
--- /dev/null
+++ b/extension/Classes/Core/Parser/TokenBuilder.php
@@ -0,0 +1,100 @@
+<?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 095326334c744cec4cebf35cbc01d56381e18f9b..34855b931a7e55ab27ea89beb3d3540be76a4495 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 b9c0c460dddda5e68218963dc4d5f3fa40982920..983b6a34bd274f23340f45fe25c80353406f4c42 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/Core/Store/Config.php b/extension/Classes/Core/Store/Config.php
index 9ec3e2d6aefa9c62f6a862ce75e70ba2f884e3c4..8c1156d0e9807e20be8af99d59676ef2867bf505 100644
--- a/extension/Classes/Core/Store/Config.php
+++ b/extension/Classes/Core/Store/Config.php
@@ -83,7 +83,7 @@ class Config {
         $absoluteConfigFilePath = $PhpUnitOverloadAbsoluteConfigFilePath === '' ? Path::absoluteConf(CONFIG_QFQ_JSON) : $PhpUnitOverloadAbsoluteConfigFilePath;
         if (!file_exists($absoluteConfigFilePath)) {
             HelperFile::createPathRecursive(Path::absoluteConf());
-            HelperFile::file_put_contents(Path::absoluteConf(CONFIG_QFQ_JSON_EXAMPLE), json_encode(self::CONFIG_REQUIRED_TEMPLATE, JSON_PRETTY_PRINT));
+            HelperFile::file_put_contents(Path::absoluteConf(CONFIG_QFQ_JSON_EXAMPLE), json_encode(self::CONFIG_REQUIRED_TEMPLATE, JSON_PRETTY_PRINT), LOCK_EX);
             Thrower::userFormException("Please create qfq config file '" . CONFIG_QFQ_JSON . "' in the conf directory which is inside the project directory. Example config file '" . CONFIG_QFQ_JSON_EXAMPLE . "' was created in conf directory.", "Project directory: " . Path::absoluteProject());
         }
         $config = HelperFile::json_decode(HelperFile::file_get_contents($absoluteConfigFilePath));
@@ -202,7 +202,7 @@ class Config {
     private static function writeConfig(array $config) {
         $absoluteConf = Path::absoluteConf();
         HelperFile::createPathRecursive($absoluteConf);
-        HelperFile::file_put_contents(Path::join($absoluteConf, CONFIG_QFQ_JSON), json_encode($config, JSON_PRETTY_PRINT));
+        HelperFile::file_put_contents(Path::join($absoluteConf, CONFIG_QFQ_JSON), json_encode($config, JSON_PRETTY_PRINT), LOCK_EX);
         chmod(Path::join($absoluteConf, CONFIG_QFQ_JSON), 0640);
     }
 
diff --git a/extension/Classes/Core/Store/Store.php b/extension/Classes/Core/Store/Store.php
index bcde3238da1791ff7d55c1dfb9c26372dfca6634..37b8cf83c3075747291700d0eedc38d29a1bdb06 100644
--- a/extension/Classes/Core/Store/Store.php
+++ b/extension/Classes/Core/Store/Store.php
@@ -519,7 +519,9 @@ class Store {
             if ($storeName === STORE_USER && $key == TYPO3_FE_USER) {
                 $qfqLogPathAbsolute = Path::absoluteQfqLogFile();
                 $feUserOld = isset($data[$key]) ? $data[$key] : self::getVar($key, STORE_TYPO3 . STORE_EMPTY);
-                Logger::logMessage(date('Y.m.d H:i:s ') . ": Switch feUser '$feUserOld' to '$value'", $qfqLogPathAbsolute);
+                if ($feUserOld !== $value) {
+                    Logger::logMessage(date('Y.m.d H:i:s ') . ": Switch feUser '$feUserOld' to '$value'", $qfqLogPathAbsolute);
+                }
             }
 
             $data[$key] = $value;
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/Resources/Private/Form/cron.json b/extension/Resources/Private/Form/cron.json
index cad94e44502adf928c18e8836939dc8d4b332764..1cb4c268c4b8cb3858405450068e36b8815d2851 100644
--- a/extension/Resources/Private/Form/cron.json
+++ b/extension/Resources/Private/Form/cron.json
@@ -316,8 +316,8 @@
             "type": "text",
             "subrecordOption": "",
             "encode": "none",
-            "checkType": "all",
-            "checkPattern": "",
+            "checkType": "pattern",
+            "checkPattern": "^(?!\\/fileadmin\\/)(?!.*\\/fileadmin\\/).*",
             "onChange": "",
             "ord": 80,
             "tabindex": 0,
@@ -334,7 +334,7 @@
             "placeholder": "",
             "value": "",
             "sql1": "",
-            "parameter": "",
+            "parameter": "data-pattern-error=Beginning slash before fileadmin is not allowed",
             "parameterLanguageA": "",
             "parameterLanguageB": "",
             "parameterLanguageC": "",
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/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 848142983e29bb2e9ddcc9e0b9ab647c57ce8e58..be32013f560e6fa6bf04c5bedcc9ae2fdf879032 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/extension/ext_localconf.php b/extension/ext_localconf.php
index 726084a9766662b399afa72ce5d3daed57a56ccf..c8e7f35721ff158ef5ede212dc35f12da20e5c62 100644
--- a/extension/ext_localconf.php
+++ b/extension/ext_localconf.php
@@ -27,7 +27,7 @@ if ($typo3VersionInteger >= 10000000) {
 }
 
 \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
-    'IMATHUZH.' . 'qfq',
+    'qfq',
     'Qfq',
     $controllerAction,
     $nonCacheableControllerAction,
diff --git a/extension/ext_tables.php b/extension/ext_tables.php
index ead3b010609e0c76f3d3719d203adbc68d601deb..9e69f1d4992264d8ed1f385594744948060257fe 100644
--- a/extension/ext_tables.php
+++ b/extension/ext_tables.php
@@ -8,7 +8,7 @@ if (!defined('TYPO3_MODE')) {
 }
 
 \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
-    'IMATHUZH.' . 'qfq',
+    'qfq',
     'Qfq',
     'QFQ Element',
     'EXT:qfq/ext_icon.png'
diff --git a/javascript/build/copy.js b/javascript/build/copy.js
new file mode 100644
index 0000000000000000000000000000000000000000..ee8e9c56826bebec5bef876b10a9fd0ca15022fa
--- /dev/null
+++ b/javascript/build/copy.js
@@ -0,0 +1,183 @@
+const { ncp } = require("ncp")
+const fs = require("fs")
+const { exec } = require("child_process");
+
+ncp.limit = 16
+
+const options = {
+    js: {
+        clobber: true,  //overwrite dir
+        stopOnErr: true,
+        filter: /(.+(?<!\..*)|.+\.debug\.js|qfq\..*\.js|.+\.min\.js|.+\.min\.js\.map)$/
+    },
+    css: {
+        clobber: true,  //overwrite dir
+        stopOnErr: true,
+        filter: /(.+(?<!\..*)|.*\.min\.css|qfq.*\.css|.*\.min\.css\.map)$/
+    },
+    font: {
+        clobber: true,  //overwrite dir
+        stopOnErr: true,
+        filter: /(.+(?<!\..*)|.*\.ttf|.*\.svg|.*\.woff|.*\.woff2)$/
+    }
+}
+
+const target = {
+    js: "extension/Resources/Public/JavaScript/",
+    css: "extension/Resources/Public/Css/",
+    font: "extension/Resources/Public/fonts"
+}
+
+const target_dev = {
+    js: "js/",
+    css: "css/"
+}
+
+const todos = [
+    {
+        name: "bootstrap",
+        js: "node_modules/bootstrap/dist/js/",
+        css: "node_modules/bootstrap/dist/css/",
+        font: "node_modules/bootstrap/dist/fonts/"
+    },{
+        name: "jquery",
+        js: "node_modules/jquery/dist/"
+    },{
+        name: "tablesorter",
+        custom: [
+            {
+                from: "node_modules/tablesorter/dist/js/jquery.tablesorter.combined.min.js",
+                to: target.js
+            },{
+                from: "node_modules/tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js",
+                to: target.js
+            },{
+                from: "node_modules/tablesorter/dist/js/widgets/widget-columnSelector.min.js",
+                to: target.js
+            },{
+                from: "node_modules/tablesorter/dist/js/widgets/widget-output.min.js",
+                to: target.js
+            }
+        ]
+    },{
+        name: "datetimepicker",
+        js: "javascript/src/Plugins/bootstrap-datetimepicker.min.js",
+        css: "javascript/src/Plugins/"
+    },{
+        name: "chart-js",
+        js: "node_modules/chart.js/dist/"
+    },{
+        name: "qfq",
+        js: "javascript/build/dist/",
+        css: "less/dist/"
+    },{
+        name: "tinymce",
+        js: 'node_modules/tinymce/',
+        custom: [
+            {
+                from: "node_modules/tinymce/skins",
+                to: target.js
+            },{
+                from: "node_modules/tinymce/plugins",
+                to: target.js
+            }
+        ]
+    },{
+        name: "qfq plugins",
+        js: "javascript/src/Plugins/",
+        css: "javascript/src/Plugins/"
+    },{
+        name: "fontAwesome",
+        css: "node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css",
+        custom: [
+            {
+                from: "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
+                to: target.css + "font-awesome.min.css"
+            },{
+                from: "node_modules/@fortawesome/fontawesome-free/webfonts",
+                to: "extension/Resources/Public/"
+            }
+        ]
+    },{
+        name: "fontPassword",
+        font: "resources/custom_fonts/"
+    },{
+        name: "typeAhead",
+        js: "node_modules/corejs-typeahead/dist/"
+    },{
+        name: "codemirror",
+        css: "node_modules/codemirror/lib/",
+        custom: [
+            {
+                from: "node_modules/codemirror/mode/",
+                to: target.js + "code-mirror-mode/"
+            },
+            { 
+                from: "node_modules/codemirror/theme/monokai.css",
+                to: target.css + "theme/"
+            },
+            {
+                from: "node_modules/codemirror/lib/codemirror.css",
+                to: target.css
+            },
+            {
+                from: "node_modules/codemirror/lib/codemirror.js",
+                to: target.js
+            }
+        ]
+    },{
+        name: "EventEmitter",
+        js: "node_modules/wolfy87-eventemitter/"
+    },{
+        name: "fullcalendar",
+        js: "node_modules/fullcalendar/dist/",
+        css: "node_modules/fullcalendar/dist/"
+    },{
+        name: "moment",
+        js: "node_modules/moment/min/",
+    },{
+        name: "jqwidgets",
+        custom: [
+            {
+                from: "node_modules/jqwidgets-framework/jqwidgets/jqx-all.js",
+                to: target.js
+            },{
+                from: "node_modules/jqwidgets-framework/jqwidgets/globalization/globalize.js",
+                to: target.js
+            },{
+                from: "node_modules/jqwidgets-framework/jqwidgets/styles/jqx.base.css",
+                to: target.css
+            },{
+                from: "node_modules/jqwidgets-framework/jqwidgets/styles/jqx.bootstrap.css",
+                to: target.css
+            }
+        ]
+    }
+]
+
+const types = ["js", "css", "font"]
+
+console.log("Async copying files:")
+for (const todo of todos) {
+    for(const type of types) {
+        if(todo.hasOwnProperty(type)) {
+            ncp(todo[type], target[type], options[type], (err) => printProgress(err, todo.name, type));
+        }
+    }
+    if(todo.hasOwnProperty("custom")) {
+        for (const job of todo.custom) {
+            if (!fs.existsSync(job.to.substring(0, job.to.lastIndexOf("/")))) {
+                fs.mkdirSync(job.to.substring(0, job.to.lastIndexOf("/")));
+              }
+            exec('cp -r "' + job.from + '" "' + job.to + '"', (error, stdout, stderr) => printProgress(error, todo.name, "custom"))
+            //ncp(job.from, job.to, options.custom, (err) => printProgress(err, todo.name, "custom"))
+        }
+    }
+}
+
+function printProgress(err, name, type) {
+    if (err) {
+        return console.error(err);
+    }
+    console.log(' * copied ' + type + ' ' + name);
+}
\ No newline at end of file
diff --git a/javascript/build/terser.js b/javascript/build/terser.js
new file mode 100644
index 0000000000000000000000000000000000000000..109f4fccbaeb75a99c70219314604990a2cabd1f
--- /dev/null
+++ b/javascript/build/terser.js
@@ -0,0 +1,60 @@
+const { minify } = require("terser");
+const fs = require('fs');
+
+const jsPath = "javascript/build/dist/"
+const extPath = "extension/Resources/Public/JavaScript/"
+const cssPath = "extension/Resources/Public/Css/"
+
+const todos = [
+    {
+        name: "qfq",
+        input: "javascript/build/dist/qfq.debug.js",
+        output: jsPath + "qfq.min.js"
+    },{
+        name: "qfqFabric",
+        input: "javascript/src/Plugins/qfq.fabric.js",
+        output: jsPath + "qfq.fabric.min.js"
+    },{
+        name: "qfqValidator",
+        input: "javascript/src/Plugins/validator.js",
+        output: jsPath + "validator.min.js"
+    },{
+        name: "codemirror",
+        input: "node_modules/codemirror/lib/codemirror.js",
+        output: extPath + "codemirror.min.js"
+    },{
+        name: "codemirror sql",
+        input: "node_modules/codemirror/lib/codemirror.js",
+        output: extPath + "code-mirror-mode/sql/sql.min.js",
+        mkdir: extPath + "code-mirror-mode/sql"
+    }
+]
+
+const defaultOptions = {
+    compress: {
+        defaults: false,
+        ecma: 2015
+    }
+};
+
+async function minifySource(input, output, options) {
+    let sourceCode = fs.readFileSync(input, 'utf8');
+    minify(sourceCode, options)
+        .then( (res) => callWriteFile(output, res))    
+}
+
+function callWriteFile(output, sourceCode) {
+    //console.log("Source Code", sourceCode)
+    fs.writeFileSync(output, sourceCode.code)
+}
+
+for (const todo of todos) {
+    console.log("minifying " + todo.name)
+    let options = defaultOptions
+    if(todo.hasOwnProperty("options")) options = todo.options
+    if(todo.hasOwnProperty("mkdir")) {
+        fs.mkdirSync(todo.mkdir, { recursive: true })
+    }
+    minifySource(todo.input, todo.output, options)
+}
+
diff --git a/javascript/src/Element/FormGroup.js b/javascript/src/Core/FormGroup.js
similarity index 100%
rename from javascript/src/Element/FormGroup.js
rename to javascript/src/Core/FormGroup.js
diff --git a/javascript/src/QfqEvents.js b/javascript/src/Core/QfqEvents.js
similarity index 100%
rename from javascript/src/QfqEvents.js
rename to javascript/src/Core/QfqEvents.js
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 ae20acd042bed4be583f27f52429e88375a1cd2d..3c9d5820699162de408a1dab38d4a015e8d518f1 100644
--- a/javascript/src/Main.js
+++ b/javascript/src/Main.js
@@ -31,7 +31,6 @@ $(document).ready( function () {
 
 
         var collection = document.getElementsByClassName("qfq-form");
-        console.log(collection);
         var qfqPages = [];
         for (const form of collection) {
             const page = new n.QfqPage(form.dataset);
diff --git a/javascript/src/Plugins/bootstrap-datetimepicker.min.css b/javascript/src/Plugins/bootstrap-datetimepicker.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..365654ba572673b19b7e707635120e02de98f935
--- /dev/null
+++ b/javascript/src/Plugins/bootstrap-datetimepicker.min.css
@@ -0,0 +1,5 @@
+/*!
+   * Bootstrap Datetime Picker v4.17.49
+   * Copyright 2015-2020 Jonathan Peterson
+   * Licensed under MIT (https://github.com/Eonasdan/bootstrap-datetimepicker/blob/master/LICENSE)
+   */.bootstrap-datetimepicker-widget{list-style:none}.bootstrap-datetimepicker-widget.dropdown-menu{display:block;margin:2px 0;padding:4px;width:19em}@media (min-width:768px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:992px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:1200px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}.bootstrap-datetimepicker-widget.dropdown-menu:before,.bootstrap-datetimepicker-widget.dropdown-menu:after{content:'';display:inline-block;position:absolute}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before{border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);top:-7px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;top:-6px;left:8px}.bootstrap-datetimepicker-widget.dropdown-menu.top:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.dropdown-menu.top:after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid white;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget .list-unstyled{margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Hours"}.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Hours"}.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Hours"}.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle AM/PM"}.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Clear the picker"}.bootstrap-datetimepicker-widget .btn[data-action="today"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Set the date to today"}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget .picker-switch::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle Date and Time Screens"}.bootstrap-datetimepicker-widget .picker-switch td{padding:0;margin:0;height:auto;width:auto;line-height:inherit}.bootstrap-datetimepicker-widget .picker-switch td span{line-height:2.5;height:2.5em;width:100%}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget table td,.bootstrap-datetimepicker-widget table th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget table th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table th.picker-switch{width:145px}.bootstrap-datetimepicker-widget table th.disabled,.bootstrap-datetimepicker-widget table th.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table th.prev::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Previous Month"}.bootstrap-datetimepicker-widget table th.next::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Next Month"}.bootstrap-datetimepicker-widget table thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#eee}.bootstrap-datetimepicker-widget table td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget table td.cw{font-size:.8em;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget table td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table td.day:hover,.bootstrap-datetimepicker-widget table td.hour:hover,.bootstrap-datetimepicker-widget table td.minute:hover,.bootstrap-datetimepicker-widget table td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget table td.old,.bootstrap-datetimepicker-widget table td.new{color:#777}.bootstrap-datetimepicker-widget table td.today{position:relative}.bootstrap-datetimepicker-widget table td.today:before{content:'';display:inline-block;border:solid transparent;border-width:0 0 7px 7px;border-bottom-color:#337ab7;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget table td.disabled,.bootstrap-datetimepicker-widget table td.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget table td span:hover{background:#eee}.bootstrap-datetimepicker-widget table td span.active{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td span.old{color:#777}.bootstrap-datetimepicker-widget table td span.disabled,.bootstrap-datetimepicker-widget table td span.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget.usetwentyfour td.hour{height:27px;line-height:27px}.bootstrap-datetimepicker-widget.wider{width:21em}.bootstrap-datetimepicker-widget .datepicker-decades .decade{line-height:1.8em !important}.input-group.date .input-group-addon{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}
\ No newline at end of file
diff --git a/javascript/src/Plugins/bootstrap-datetimepicker.min.js b/javascript/src/Plugins/bootstrap-datetimepicker.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..884a8f879923ac55f9046afc164b592c41dfae46
--- /dev/null
+++ b/javascript/src/Plugins/bootstrap-datetimepicker.min.js
@@ -0,0 +1 @@
+!function(e){"use strict";if("function"==typeof define&&define.amd)define(["jquery","moment"],e);else if("object"==typeof exports)module.exports=e(require("jquery"),require("moment"));else{if("undefined"==typeof jQuery)throw"bootstrap-datetimepicker requires jQuery to be loaded first";if("undefined"==typeof moment)throw"bootstrap-datetimepicker requires Moment.js to be loaded first";e(jQuery,moment)}}(function($,_){"use strict";if(!_)throw new Error("bootstrap-datetimepicker requires Moment.js to be loaded first");function i(i,p){function a(){return void 0!==_.tz&&void 0!==p.timeZone&&null!==p.timeZone&&""!==p.timeZone}function c(e){var t;return t=null==e?_():_.isDate(e)||_.isMoment(e)?_(e):a()?_.tz(e,B,p.useStrict,p.timeZone):_(e,B,p.useStrict),a()&&t.tz(p.timeZone),t}function d(e){if("string"!=typeof e||1<e.length)throw new TypeError("isEnabled expects a single character string parameter");switch(e){case"y":return-1!==q.indexOf("Y");case"M":return-1!==q.indexOf("M");case"d":return-1!==q.toLowerCase().indexOf("d");case"h":case"H":return-1!==q.toLowerCase().indexOf("h");case"m":return-1!==q.indexOf("m");case"s":return-1!==q.indexOf("s");default:return!1}}function l(){return d("h")||d("m")||d("s")}function u(){return d("y")||d("M")||d("d")}function f(){var e,t,a,n=$("<div>").addClass("timepicker-hours").append($("<table>").addClass("table-condensed")),r=$("<div>").addClass("timepicker-minutes").append($("<table>").addClass("table-condensed")),i=$("<div>").addClass("timepicker-seconds").append($("<table>").addClass("table-condensed")),o=[(e=$("<tr>"),t=$("<tr>"),a=$("<tr>"),d("h")&&(e.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.incrementHour}).addClass("btn").attr("data-action","incrementHours").append($("<span>").addClass(p.icons.up)))),t.append($("<td>").append($("<span>").addClass("timepicker-hour").attr({"data-time-component":"hours",title:p.tooltips.pickHour}).attr("data-action","showHours"))),a.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.decrementHour}).addClass("btn").attr("data-action","decrementHours").append($("<span>").addClass(p.icons.down))))),d("m")&&(d("h")&&(e.append($("<td>").addClass("separator")),t.append($("<td>").addClass("separator").html(":")),a.append($("<td>").addClass("separator"))),e.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.incrementMinute}).addClass("btn").attr("data-action","incrementMinutes").append($("<span>").addClass(p.icons.up)))),t.append($("<td>").append($("<span>").addClass("timepicker-minute").attr({"data-time-component":"minutes",title:p.tooltips.pickMinute}).attr("data-action","showMinutes"))),a.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.decrementMinute}).addClass("btn").attr("data-action","decrementMinutes").append($("<span>").addClass(p.icons.down))))),d("s")&&(d("m")&&(e.append($("<td>").addClass("separator")),t.append($("<td>").addClass("separator").html(":")),a.append($("<td>").addClass("separator"))),e.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.incrementSecond}).addClass("btn").attr("data-action","incrementSeconds").append($("<span>").addClass(p.icons.up)))),t.append($("<td>").append($("<span>").addClass("timepicker-second").attr({"data-time-component":"seconds",title:p.tooltips.pickSecond}).attr("data-action","showSeconds"))),a.append($("<td>").append($("<a>").attr({href:"#",tabindex:"-1",title:p.tooltips.decrementSecond}).addClass("btn").attr("data-action","decrementSeconds").append($("<span>").addClass(p.icons.down))))),Y||(e.append($("<td>").addClass("separator")),t.append($("<td>").append($("<button>").addClass("btn btn-primary").attr({"data-action":"togglePeriod",tabindex:"-1",title:p.tooltips.togglePeriod}))),a.append($("<td>").addClass("separator"))),$("<div>").addClass("timepicker-picker").append($("<table>").addClass("table-condensed").append([e,t,a])))];return d("h")&&o.push(n),d("m")&&o.push(r),d("s")&&o.push(i),o}function t(){var e,t,a,n=$("<div>").addClass("bootstrap-datetimepicker-widget dropdown-menu"),r=$("<div>").addClass("datepicker").append((t=$("<thead>").append($("<tr>").append($("<th>").addClass("prev").attr("data-action","previous").append($("<span>").addClass(p.icons.previous))).append($("<th>").addClass("picker-switch").attr("data-action","pickerSwitch").attr("colspan",p.calendarWeeks?"6":"5")).append($("<th>").addClass("next").attr("data-action","next").append($("<span>").addClass(p.icons.next)))),a=$("<tbody>").append($("<tr>").append($("<td>").attr("colspan",p.calendarWeeks?"8":"7"))),[$("<div>").addClass("datepicker-days").append($("<table>").addClass("table-condensed").append(t).append($("<tbody>"))),$("<div>").addClass("datepicker-months").append($("<table>").addClass("table-condensed").append(t.clone()).append(a.clone())),$("<div>").addClass("datepicker-years").append($("<table>").addClass("table-condensed").append(t.clone()).append(a.clone())),$("<div>").addClass("datepicker-decades").append($("<table>").addClass("table-condensed").append(t.clone()).append(a.clone()))])),i=$("<div>").addClass("timepicker").append(f()),o=$("<ul>").addClass("list-unstyled"),s=$("<li>").addClass("picker-switch"+(p.collapse?" accordion-toggle":"")).append((e=[],p.showTodayButton&&e.push($("<td>").append($("<a>").attr({"data-action":"today",title:p.tooltips.today}).append($("<span>").addClass(p.icons.today)))),!p.sideBySide&&u()&&l()&&e.push($("<td>").append($("<a>").attr({"data-action":"togglePicker",title:p.tooltips.selectTime}).append($("<span>").addClass(p.icons.time)))),p.showClear&&e.push($("<td>").append($("<a>").attr({"data-action":"clear",title:p.tooltips.clear}).append($("<span>").addClass(p.icons.clear)))),p.showClose&&e.push($("<td>").append($("<a>").attr({"data-action":"close",title:p.tooltips.close}).append($("<span>").addClass(p.icons.close)))),$("<table>").addClass("table-condensed").append($("<tbody>").append($("<tr>").append(e)))));return p.inline&&n.removeClass("dropdown-menu"),Y&&n.addClass("usetwentyfour"),d("s")&&!Y&&n.addClass("wider"),p.sideBySide&&u()&&l()?(n.addClass("timepicker-sbs"),"top"===p.toolbarPlacement&&n.append(s),n.append($("<div>").addClass("row").append(r.addClass("col-md-6")).append(i.addClass("col-md-6"))),"bottom"===p.toolbarPlacement&&n.append(s),n):("top"===p.toolbarPlacement&&o.append(s),u()&&o.append($("<li>").addClass(p.collapse&&l()?"collapse in":"").append(r)),"default"===p.toolbarPlacement&&o.append(s),l()&&o.append($("<li>").addClass(p.collapse&&u()?"collapse":"").append(i)),"bottom"===p.toolbarPlacement&&o.append(s),n.append(o))}function n(){var e,t=(z||i).position(),a=(z||i).offset(),n=p.widgetPositioning.vertical,r=p.widgetPositioning.horizontal;if(p.widgetParent)e=p.widgetParent.append(N);else if(i.is("input"))e=i.after(N).parent();else{if(p.inline)return void(e=i.append(N));(e=i).children().first().after(N)}if("auto"===n&&(n=a.top+1.5*N.height()>=$(window).height()+$(window).scrollTop()&&N.height()+i.outerHeight()<a.top?"top":"bottom"),"auto"===r&&(r=e.width()<a.left+N.outerWidth()/2&&a.left+N.outerWidth()>$(window).width()?"right":"left"),"top"===n?N.addClass("top").removeClass("bottom"):N.addClass("bottom").removeClass("top"),"right"===r?N.addClass("pull-right"):N.removeClass("pull-right"),"static"===e.css("position")&&(e=e.parents().filter(function(){return"static"!==$(this).css("position")}).first()),0===e.length)throw new Error("datetimepicker component should be placed within a non-static positioned container");N.css({top:"top"===n?"auto":t.top+i.outerHeight(),bottom:"top"===n?e.outerHeight()-(e===i?0:t.top):"auto",left:"left"===r?e===i?0:t.left:"auto",right:"left"===r?"auto":e.outerWidth()-i.outerWidth()-(e===i?0:t.left)})}function h(e){"dp.change"===e.type&&(e.date&&e.date.isSame(e.oldDate)||!e.date&&!e.oldDate)||i.trigger(e)}function r(e){"y"===e&&(e="YYYY"),h({type:"dp.update",change:e,viewDate:H.clone()})}function o(e){N&&(e&&(j=Math.max(V,Math.min(3,j+e))),N.find(".datepicker > div").hide().filter(".datepicker-"+Z[j].clsName).show())}function m(e,t){var a,n,r,i;if(e.isValid()&&!(p.disabledDates&&"d"===t&&(a=e,!0===p.disabledDates[a.format("YYYY-MM-DD")])||p.enabledDates&&"d"===t&&(n=e,!0!==p.enabledDates[n.format("YYYY-MM-DD")])||p.minDate&&e.isBefore(p.minDate,t)||p.maxDate&&e.isAfter(p.maxDate,t)||p.daysOfWeekDisabled&&"d"===t&&-1!==p.daysOfWeekDisabled.indexOf(e.day())||p.disabledHours&&("h"===t||"m"===t||"s"===t)&&(r=e,!0===p.disabledHours[r.format("H")])||p.enabledHours&&("h"===t||"m"===t||"s"===t)&&(i=e,!0!==p.enabledHours[i.format("H")]))){if(p.disabledTimeIntervals&&("h"===t||"m"===t||"s"===t)){var o=!1;if($.each(p.disabledTimeIntervals,function(){if(e.isBetween(this[0],this[1]))return!(o=!0)}),o)return}return 1}}function s(){var e,t,a,n=N.find(".datepicker-days"),r=n.find("th"),i=[],o=[];if(u()){for(r.eq(0).find("span").attr("title",p.tooltips.prevMonth),r.eq(1).attr("title",p.tooltips.selectMonth),r.eq(2).find("span").attr("title",p.tooltips.nextMonth),n.find(".disabled").removeClass("disabled"),r.eq(1).text(H.format(p.dayViewHeaderFormat)),m(H.clone().subtract(1,"M"),"M")||r.eq(0).addClass("disabled"),m(H.clone().add(1,"M"),"M")||r.eq(2).addClass("disabled"),e=H.clone().startOf("M").startOf("w").startOf("d"),a=0;a<42;a++)0===e.weekday()&&(t=$("<tr>"),p.calendarWeeks&&t.append('<td class="cw">'+e.week()+"</td>"),i.push(t)),o=["day"],e.isBefore(H,"M")&&o.push("old"),e.isAfter(H,"M")&&o.push("new"),e.isSame(E,"d")&&!W&&o.push("active"),m(e,"d")||o.push("disabled"),e.isSame(c(),"d")&&o.push("today"),0!==e.day()&&6!==e.day()||o.push("weekend"),h({type:"dp.classify",date:e,classNames:o}),t.append('<td data-action="selectDay" data-day="'+e.format("L")+'" class="'+o.join(" ")+'">'+e.date()+"</td>"),e.add(1,"d");var s,d,l;n.find("tbody").empty().append(i),s=N.find(".datepicker-months"),d=s.find("th"),l=s.find("tbody").find("span"),d.eq(0).find("span").attr("title",p.tooltips.prevYear),d.eq(1).attr("title",p.tooltips.selectYear),d.eq(2).find("span").attr("title",p.tooltips.nextYear),s.find(".disabled").removeClass("disabled"),m(H.clone().subtract(1,"y"),"y")||d.eq(0).addClass("disabled"),d.eq(1).text(H.year()),m(H.clone().add(1,"y"),"y")||d.eq(2).addClass("disabled"),l.removeClass("active"),E.isSame(H,"y")&&!W&&l.eq(E.month()).addClass("active"),l.each(function(e){m(H.clone().month(e),"M")||$(this).addClass("disabled")}),function(){var e=N.find(".datepicker-years"),t=e.find("th"),a=H.clone().subtract(5,"y"),n=H.clone().add(6,"y"),r="";for(t.eq(0).find("span").attr("title",p.tooltips.prevDecade),t.eq(1).attr("title",p.tooltips.selectDecade),t.eq(2).find("span").attr("title",p.tooltips.nextDecade),e.find(".disabled").removeClass("disabled"),p.minDate&&p.minDate.isAfter(a,"y")&&t.eq(0).addClass("disabled"),t.eq(1).text(a.year()+"-"+n.year()),p.maxDate&&p.maxDate.isBefore(n,"y")&&t.eq(2).addClass("disabled");!a.isAfter(n,"y");)r+='<span data-action="selectYear" class="year'+(a.isSame(E,"y")&&!W?" active":"")+(m(a,"y")?"":" disabled")+'">'+a.year()+"</span>",a.add(1,"y");e.find("td").html(r)}(),function(){var e,t=N.find(".datepicker-decades"),a=t.find("th"),n=_({y:H.year()-H.year()%100-1}),r=n.clone().add(100,"y"),i=n.clone(),o=!1,s=!1,d="";for(a.eq(0).find("span").attr("title",p.tooltips.prevCentury),a.eq(2).find("span").attr("title",p.tooltips.nextCentury),t.find(".disabled").removeClass("disabled"),(n.isSame(_({y:1900}))||p.minDate&&p.minDate.isAfter(n,"y"))&&a.eq(0).addClass("disabled"),a.eq(1).text(n.year()+"-"+r.year()),(n.isSame(_({y:2e3}))||p.maxDate&&p.maxDate.isBefore(r,"y"))&&a.eq(2).addClass("disabled");!n.isAfter(r,"y");)e=n.year()+12,o=p.minDate&&p.minDate.isAfter(n,"y")&&p.minDate.year()<=e,s=p.maxDate&&p.maxDate.isAfter(n,"y")&&p.maxDate.year()<=e,d+='<span data-action="selectDecade" class="decade'+(E.isAfter(n)&&E.year()<=e?" active":"")+(m(n,"y")||o||s?"":" disabled")+'" data-selection="'+(n.year()+6)+'">'+(n.year()+1)+" - "+(n.year()+12)+"</span>",n.add(12,"y");d+="<span></span><span></span><span></span>",t.find("td").html(d),a.eq(1).text(i.year()+1+"-"+n.year())}()}}function e(){var e,t,a=N.find(".timepicker span[data-time-component]");Y||(e=N.find(".timepicker [data-action=togglePeriod]"),t=E.clone().add(12<=E.hours()?-12:12,"h"),e.text(E.format("A")),m(t,"h")?e.removeClass("disabled"):e.addClass("disabled")),a.filter("[data-time-component=hours]").text(E.format(Y?"HH":"hh")),a.filter("[data-time-component=minutes]").text(E.format("mm")),a.filter("[data-time-component=seconds]").text(E.format("ss")),function(){var e=N.find(".timepicker-hours table"),t=H.clone().startOf("d"),a=[],n=$("<tr>");for(11<H.hour()&&!Y&&t.hour(12);t.isSame(H,"d")&&(Y||H.hour()<12&&t.hour()<12||11<H.hour());)t.hour()%4==0&&(n=$("<tr>"),a.push(n)),n.append('<td data-action="selectHour" class="hour'+(m(t,"h")?"":" disabled")+'">'+t.format(Y?"HH":"hh")+"</td>"),t.add(1,"h");e.empty().append(a)}(),function(){for(var e=N.find(".timepicker-minutes table"),t=H.clone().startOf("h"),a=[],n=$("<tr>"),r=1===p.stepping?5:p.stepping;H.isSame(t,"h");)t.minute()%(4*r)==0&&(n=$("<tr>"),a.push(n)),n.append('<td data-action="selectMinute" class="minute'+(m(t,"m")?"":" disabled")+'">'+t.format("mm")+"</td>"),t.add(r,"m");e.empty().append(a)}(),function(){for(var e=N.find(".timepicker-seconds table"),t=H.clone().startOf("m"),a=[],n=$("<tr>");H.isSame(t,"m");)t.second()%20==0&&(n=$("<tr>"),a.push(n)),n.append('<td data-action="selectSecond" class="second'+(m(t,"s")?"":" disabled")+'">'+t.format("ss")+"</td>"),t.add(5,"s");e.empty().append(a)}()}function y(){N&&(s(),e())}function g(e){var t=W?null:E;if(!e)return W=!0,I.val(""),i.data("date",""),h({type:"dp.change",date:!1,oldDate:t}),void y();if(e=e.clone().locale(p.locale),a()&&e.tz(p.timeZone),1!==p.stepping)for(e.minutes(Math.round(e.minutes()/p.stepping)*p.stepping).seconds(0);p.minDate&&e.isBefore(p.minDate);)e.add(p.stepping,"minutes");m(e)?(H=(E=e).clone(),I.val(E.format(q)),i.data("date",E.format(q)),W=!1,y(),h({type:"dp.change",date:E.clone(),oldDate:t})):(p.keepInvalid?h({type:"dp.change",date:e,oldDate:t}):I.val(W?"":E.format(q)),h({type:"dp.error",date:e,oldDate:t}))}function b(){var t=!1;return N?(N.find(".collapse").each(function(){var e=$(this).data("collapse");return!e||!e.transitioning||!(t=!0)}),t||(z&&z.hasClass("btn")&&z.toggleClass("active"),N.hide(),$(window).off("resize",n),N.off("click","[data-action]"),N.off("mousedown",!1),N.remove(),N=!1,h({type:"dp.hide",date:E.clone()}),I.blur(),H=E.clone()),L):L}function w(){g(null)}function v(e){return void 0===p.parseInputDate?(!_.isMoment(e)||e instanceof Date)&&(e=c(e)):e=p.parseInputDate(e),e}function k(e){return $(e.currentTarget).is(".disabled")||X[$(e.currentTarget).data("action")].apply(L,arguments),!1}function D(){var e;return I.prop("disabled")||!p.ignoreReadonly&&I.prop("readonly")||N||(void 0!==I.val()&&0!==I.val().trim().length?g(v(I.val().trim())):W&&p.useCurrent&&(p.inline||I.is("input")&&0===I.val().trim().length)&&(e=c(),"string"==typeof p.useCurrent&&(e={year:function(e){return e.month(0).date(1).hours(0).seconds(0).minutes(0)},month:function(e){return e.date(1).hours(0).seconds(0).minutes(0)},day:function(e){return e.hours(0).seconds(0).minutes(0)},hour:function(e){return e.seconds(0).minutes(0)},minute:function(e){return e.seconds(0)}}[p.useCurrent](e)),g(e)),N=t(),function(){var e=$("<tr>"),t=H.clone().startOf("w").startOf("d");for(!0===p.calendarWeeks&&e.append($("<th>").addClass("cw").text("#"));t.isBefore(H.clone().endOf("w"));)e.append($("<th>").addClass("dow").text(t.format("dd"))),t.add(1,"d");N.find(".datepicker-days thead").append(e)}(),function(){for(var e=[],t=H.clone().startOf("y").startOf("d");t.isSame(H,"y");)e.push($("<span>").attr("data-action","selectMonth").addClass("month").text(t.format("MMM"))),t.add(1,"M");N.find(".datepicker-months td").empty().append(e)}(),N.find(".timepicker-hours").hide(),N.find(".timepicker-minutes").hide(),N.find(".timepicker-seconds").hide(),y(),o(),$(window).on("resize",n),N.on("click","[data-action]",k),N.on("mousedown",!1),z&&z.hasClass("btn")&&z.toggleClass("active"),n(),N.show(),p.focusOnShow&&!I.is(":focus")&&I.focus(),h({type:"dp.show"})),L}function C(){return(N?b:D)()}function x(e){var t,a,n,r,i=null,o=[],s={},d=e.which;for(t in K[d]="p",K)K.hasOwnProperty(t)&&"p"===K[t]&&(o.push(t),parseInt(t,10)!==d&&(s[t]=!0));for(t in p.keyBinds)if(p.keyBinds.hasOwnProperty(t)&&"function"==typeof p.keyBinds[t]&&(n=t.split(" ")).length===o.length&&J[d]===n[n.length-1]){for(r=!0,a=n.length-2;0<=a;a--)if(!(J[n[a]]in s)){r=!1;break}if(r){i=p.keyBinds[t];break}}i&&(i.call(L,N),e.stopPropagation(),e.preventDefault())}function T(e){K[e.which]="r",e.stopPropagation(),e.preventDefault()}function M(e){var t=$(e.target).val().trim(),a=t?v(t):null;return g(a),e.stopImmediatePropagation(),!1}function S(e){var t={};return $.each(e,function(){var e=v(this);e.isValid()&&(t[e.format("YYYY-MM-DD")]=!0)}),!!Object.keys(t).length&&t}function O(e){var t={};return $.each(e,function(){t[this]=!0}),!!Object.keys(t).length&&t}function P(){var e=p.format||"L LT";q=e.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(e){return(E.localeData().longDateFormat(e)||e).replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(e){return E.localeData().longDateFormat(e)||e})}),(B=p.extraFormats?p.extraFormats.slice():[]).indexOf(e)<0&&B.indexOf(q)<0&&B.push(q),Y=q.toLowerCase().indexOf("a")<1&&q.replace(/\[.*?\]/g,"").indexOf("h")<1,d("y")&&(V=2),d("M")&&(V=1),d("d")&&(V=0),j=Math.max(V,j),W||g(E)}var E,H,I,Y,q,B,j,A,F,L={},W=!0,z=!1,N=!1,V=0,Z=[{clsName:"days",navFnc:"M",navStep:1},{clsName:"months",navFnc:"y",navStep:1},{clsName:"years",navFnc:"y",navStep:10},{clsName:"decades",navFnc:"y",navStep:100}],R=["days","months","years","decades"],Q=["top","bottom","auto"],U=["left","right","auto"],G=["default","top","bottom"],J={up:38,38:"up",down:40,40:"down",left:37,37:"left",right:39,39:"right",tab:9,9:"tab",escape:27,27:"escape",enter:13,13:"enter",pageUp:33,33:"pageUp",pageDown:34,34:"pageDown",shift:16,16:"shift",control:17,17:"control",space:32,32:"space",t:84,84:"t",delete:46,46:"delete"},K={},X={next:function(){var e=Z[j].navFnc;H.add(Z[j].navStep,e),s(),r(e)},previous:function(){var e=Z[j].navFnc;H.subtract(Z[j].navStep,e),s(),r(e)},pickerSwitch:function(){o(1)},selectMonth:function(e){var t=$(e.target).closest("tbody").find("span").index($(e.target));H.month(t),j===V?(g(E.clone().year(H.year()).month(H.month())),p.inline||b()):(o(-1),s()),r("M")},selectYear:function(e){var t=parseInt($(e.target).text(),10)||0;H.year(t),j===V?(g(E.clone().year(H.year())),p.inline||b()):(o(-1),s()),r("YYYY")},selectDecade:function(e){var t=parseInt($(e.target).data("selection"),10)||0;H.year(t),j===V?(g(E.clone().year(H.year())),p.inline||b()):(o(-1),s()),r("YYYY")},selectDay:function(e){var t=H.clone();$(e.target).is(".old")&&t.subtract(1,"M"),$(e.target).is(".new")&&t.add(1,"M"),g(t.date(parseInt($(e.target).text(),10))),l()||p.keepOpen||p.inline||b()},incrementHours:function(){var e=E.clone().add(1,"h");m(e,"h")&&g(e)},incrementMinutes:function(){var e=E.clone().add(p.stepping,"m");m(e,"m")&&g(e)},incrementSeconds:function(){var e=E.clone().add(1,"s");m(e,"s")&&g(e)},decrementHours:function(){var e=E.clone().subtract(1,"h");m(e,"h")&&g(e)},decrementMinutes:function(){var e=E.clone().subtract(p.stepping,"m");m(e,"m")&&g(e)},decrementSeconds:function(){var e=E.clone().subtract(1,"s");m(e,"s")&&g(e)},togglePeriod:function(){g(E.clone().add(12<=E.hours()?-12:12,"h"))},togglePicker:function(e){var t,a=$(e.target),n=a.closest("ul"),r=n.find(".in"),i=n.find(".collapse:not(.in)");if(r&&r.length){if((t=r.data("collapse"))&&t.transitioning)return;r.collapse?(r.collapse("hide"),i.collapse("show")):(r.removeClass("in"),i.addClass("in")),a.is("span")?a.toggleClass(p.icons.time+" "+p.icons.date):a.find("span").toggleClass(p.icons.time+" "+p.icons.date)}},showPicker:function(){N.find(".timepicker > div:not(.timepicker-picker)").hide(),N.find(".timepicker .timepicker-picker").show()},showHours:function(){N.find(".timepicker .timepicker-picker").hide(),N.find(".timepicker .timepicker-hours").show()},showMinutes:function(){N.find(".timepicker .timepicker-picker").hide(),N.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){N.find(".timepicker .timepicker-picker").hide(),N.find(".timepicker .timepicker-seconds").show()},selectHour:function(e){var t=parseInt($(e.target).text(),10);Y||(12<=E.hours()?12!==t&&(t+=12):12===t&&(t=0)),g(E.clone().hours(t)),X.showPicker.call(L)},selectMinute:function(e){g(E.clone().minutes(parseInt($(e.target).text(),10))),X.showPicker.call(L)},selectSecond:function(e){g(E.clone().seconds(parseInt($(e.target).text(),10))),X.showPicker.call(L)},clear:w,today:function(){var e=c();m(e,"d")&&g(e)},close:b};if(L.destroy=function(){b(),I.off({change:M,blur:blur,keydown:x,keyup:T,focus:p.allowInputToggle?b:""}),i.is("input")?I.off({focus:D}):z&&(z.off("click",C),z.off("mousedown",!1)),i.removeData("DateTimePicker"),i.removeData("date")},L.toggle=C,L.show=D,L.hide=b,L.disable=function(){return b(),z&&z.hasClass("btn")&&z.addClass("disabled"),I.prop("disabled",!0),L},L.enable=function(){return z&&z.hasClass("btn")&&z.removeClass("disabled"),I.prop("disabled",!1),L},L.ignoreReadonly=function(e){if(0===arguments.length)return p.ignoreReadonly;if("boolean"!=typeof e)throw new TypeError("ignoreReadonly () expects a boolean parameter");return p.ignoreReadonly=e,L},L.options=function(e){if(0===arguments.length)return $.extend(!0,{},p);if(!(e instanceof Object))throw new TypeError("options() options parameter should be an object");return $.extend(!0,p,e),$.each(p,function(e,t){if(void 0===L[e])throw new TypeError("option "+e+" is not recognized!");L[e](t)}),L},L.date=function(e){if(0===arguments.length)return W?null:E.clone();if(!(null===e||"string"==typeof e||_.isMoment(e)||e instanceof Date))throw new TypeError("date() parameter must be one of [null, string, moment or Date]");return g(null===e?null:v(e)),L},L.format=function(e){if(0===arguments.length)return p.format;if("string"!=typeof e&&("boolean"!=typeof e||!1!==e))throw new TypeError("format() expects a string or boolean:false parameter "+e);return p.format=e,q&&P(),L},L.timeZone=function(e){if(0===arguments.length)return p.timeZone;if("string"!=typeof e)throw new TypeError("newZone() expects a string parameter");return p.timeZone=e,L},L.dayViewHeaderFormat=function(e){if(0===arguments.length)return p.dayViewHeaderFormat;if("string"!=typeof e)throw new TypeError("dayViewHeaderFormat() expects a string parameter");return p.dayViewHeaderFormat=e,L},L.extraFormats=function(e){if(0===arguments.length)return p.extraFormats;if(!1!==e&&!(e instanceof Array))throw new TypeError("extraFormats() expects an array or false parameter");return p.extraFormats=e,B&&P(),L},L.disabledDates=function(e){if(0===arguments.length)return p.disabledDates?$.extend({},p.disabledDates):p.disabledDates;if(!e)return p.disabledDates=!1,y(),L;if(!(e instanceof Array))throw new TypeError("disabledDates() expects an array parameter");return p.disabledDates=S(e),p.enabledDates=!1,y(),L},L.enabledDates=function(e){if(0===arguments.length)return p.enabledDates?$.extend({},p.enabledDates):p.enabledDates;if(!e)return p.enabledDates=!1,y(),L;if(!(e instanceof Array))throw new TypeError("enabledDates() expects an array parameter");return p.enabledDates=S(e),p.disabledDates=!1,y(),L},L.daysOfWeekDisabled=function(e){if(0===arguments.length)return p.daysOfWeekDisabled.splice(0);if("boolean"==typeof e&&!e)return p.daysOfWeekDisabled=!1,y(),L;if(!(e instanceof Array))throw new TypeError("daysOfWeekDisabled() expects an array parameter");if(p.daysOfWeekDisabled=e.reduce(function(e,t){return 6<(t=parseInt(t,10))||t<0||isNaN(t)||-1===e.indexOf(t)&&e.push(t),e},[]).sort(),p.useCurrent&&!p.keepInvalid){for(var t=0;!m(E,"d");){if(E.add(1,"d"),31===t)throw"Tried 31 times to find a valid date";t++}g(E)}return y(),L},L.maxDate=function(e){if(0===arguments.length)return p.maxDate?p.maxDate.clone():p.maxDate;if("boolean"==typeof e&&!1===e)return p.maxDate=!1,y(),L;"string"==typeof e&&("now"!==e&&"moment"!==e||(e=c()));var t=v(e);if(!t.isValid())throw new TypeError("maxDate() Could not parse date parameter: "+e);if(p.minDate&&t.isBefore(p.minDate))throw new TypeError("maxDate() date parameter is before options.minDate: "+t.format(q));return p.maxDate=t,p.useCurrent&&!p.keepInvalid&&E.isAfter(e)&&g(p.maxDate),H.isAfter(t)&&(H=t.clone().subtract(p.stepping,"m")),y(),L},L.minDate=function(e){if(0===arguments.length)return p.minDate?p.minDate.clone():p.minDate;if("boolean"==typeof e&&!1===e)return p.minDate=!1,y(),L;"string"==typeof e&&("now"!==e&&"moment"!==e||(e=c()));var t=v(e);if(!t.isValid())throw new TypeError("minDate() Could not parse date parameter: "+e);if(p.maxDate&&t.isAfter(p.maxDate))throw new TypeError("minDate() date parameter is after options.maxDate: "+t.format(q));return p.minDate=t,p.useCurrent&&!p.keepInvalid&&E.isBefore(e)&&g(p.minDate),H.isBefore(t)&&(H=t.clone().add(p.stepping,"m")),y(),L},L.defaultDate=function(e){if(0===arguments.length)return p.defaultDate?p.defaultDate.clone():p.defaultDate;if(!e)return p.defaultDate=!1,L;"string"==typeof e&&(e="now"===e||"moment"===e?c():c(e));var t=v(e);if(!t.isValid())throw new TypeError("defaultDate() Could not parse date parameter: "+e);if(!m(t))throw new TypeError("defaultDate() date passed is invalid according to component setup validations");return p.defaultDate=t,(p.defaultDate&&p.inline||""===I.val().trim())&&g(p.defaultDate),L},L.locale=function(e){if(0===arguments.length)return p.locale;if(!_.localeData(e))throw new TypeError("locale() locale "+e+" is not loaded from moment locales!");return p.locale=e,E.locale(p.locale),H.locale(p.locale),q&&P(),N&&(b(),D()),L},L.stepping=function(e){return 0===arguments.length?p.stepping:(e=parseInt(e,10),(isNaN(e)||e<1)&&(e=1),p.stepping=e,L)},L.useCurrent=function(e){var t=["year","month","day","hour","minute"];if(0===arguments.length)return p.useCurrent;if("boolean"!=typeof e&&"string"!=typeof e)throw new TypeError("useCurrent() expects a boolean or string parameter");if("string"==typeof e&&-1===t.indexOf(e.toLowerCase()))throw new TypeError("useCurrent() expects a string parameter of "+t.join(", "));return p.useCurrent=e,L},L.collapse=function(e){if(0===arguments.length)return p.collapse;if("boolean"!=typeof e)throw new TypeError("collapse() expects a boolean parameter");return p.collapse===e||(p.collapse=e,N&&(b(),D())),L},L.icons=function(e){if(0===arguments.length)return $.extend({},p.icons);if(!(e instanceof Object))throw new TypeError("icons() expects parameter to be an Object");return $.extend(p.icons,e),N&&(b(),D()),L},L.tooltips=function(e){if(0===arguments.length)return $.extend({},p.tooltips);if(!(e instanceof Object))throw new TypeError("tooltips() expects parameter to be an Object");return $.extend(p.tooltips,e),N&&(b(),D()),L},L.useStrict=function(e){if(0===arguments.length)return p.useStrict;if("boolean"!=typeof e)throw new TypeError("useStrict() expects a boolean parameter");return p.useStrict=e,L},L.sideBySide=function(e){if(0===arguments.length)return p.sideBySide;if("boolean"!=typeof e)throw new TypeError("sideBySide() expects a boolean parameter");return p.sideBySide=e,N&&(b(),D()),L},L.viewMode=function(e){if(0===arguments.length)return p.viewMode;if("string"!=typeof e)throw new TypeError("viewMode() expects a string parameter");if(-1===R.indexOf(e))throw new TypeError("viewMode() parameter must be one of ("+R.join(", ")+") value");return p.viewMode=e,j=Math.max(R.indexOf(e),V),o(),L},L.toolbarPlacement=function(e){if(0===arguments.length)return p.toolbarPlacement;if("string"!=typeof e)throw new TypeError("toolbarPlacement() expects a string parameter");if(-1===G.indexOf(e))throw new TypeError("toolbarPlacement() parameter must be one of ("+G.join(", ")+") value");return p.toolbarPlacement=e,N&&(b(),D()),L},L.widgetPositioning=function(e){if(0===arguments.length)return $.extend({},p.widgetPositioning);if("[object Object]"!=={}.toString.call(e))throw new TypeError("widgetPositioning() expects an object variable");if(e.horizontal){if("string"!=typeof e.horizontal)throw new TypeError("widgetPositioning() horizontal variable must be a string");if(e.horizontal=e.horizontal.toLowerCase(),-1===U.indexOf(e.horizontal))throw new TypeError("widgetPositioning() expects horizontal parameter to be one of ("+U.join(", ")+")");p.widgetPositioning.horizontal=e.horizontal}if(e.vertical){if("string"!=typeof e.vertical)throw new TypeError("widgetPositioning() vertical variable must be a string");if(e.vertical=e.vertical.toLowerCase(),-1===Q.indexOf(e.vertical))throw new TypeError("widgetPositioning() expects vertical parameter to be one of ("+Q.join(", ")+")");p.widgetPositioning.vertical=e.vertical}return y(),L},L.calendarWeeks=function(e){if(0===arguments.length)return p.calendarWeeks;if("boolean"!=typeof e)throw new TypeError("calendarWeeks() expects parameter to be a boolean value");return p.calendarWeeks=e,y(),L},L.showTodayButton=function(e){if(0===arguments.length)return p.showTodayButton;if("boolean"!=typeof e)throw new TypeError("showTodayButton() expects a boolean parameter");return p.showTodayButton=e,N&&(b(),D()),L},L.showClear=function(e){if(0===arguments.length)return p.showClear;if("boolean"!=typeof e)throw new TypeError("showClear() expects a boolean parameter");return p.showClear=e,N&&(b(),D()),L},L.widgetParent=function(e){if(0===arguments.length)return p.widgetParent;if("string"==typeof e&&(e=$(e)),null!==e&&"string"!=typeof e&&!(e instanceof $))throw new TypeError("widgetParent() expects a string or a jQuery object parameter");return p.widgetParent=e,N&&(b(),D()),L},L.keepOpen=function(e){if(0===arguments.length)return p.keepOpen;if("boolean"!=typeof e)throw new TypeError("keepOpen() expects a boolean parameter");return p.keepOpen=e,L},L.focusOnShow=function(e){if(0===arguments.length)return p.focusOnShow;if("boolean"!=typeof e)throw new TypeError("focusOnShow() expects a boolean parameter");return p.focusOnShow=e,L},L.inline=function(e){if(0===arguments.length)return p.inline;if("boolean"!=typeof e)throw new TypeError("inline() expects a boolean parameter");return p.inline=e,L},L.clear=function(){return w(),L},L.keyBinds=function(e){return 0===arguments.length?p.keyBinds:(p.keyBinds=e,L)},L.getMoment=function(e){return c(e)},L.debug=function(e){if("boolean"!=typeof e)throw new TypeError("debug() expects a boolean parameter");return p.debug=e,L},L.allowInputToggle=function(e){if(0===arguments.length)return p.allowInputToggle;if("boolean"!=typeof e)throw new TypeError("allowInputToggle() expects a boolean parameter");return p.allowInputToggle=e,L},L.showClose=function(e){if(0===arguments.length)return p.showClose;if("boolean"!=typeof e)throw new TypeError("showClose() expects a boolean parameter");return p.showClose=e,L},L.keepInvalid=function(e){if(0===arguments.length)return p.keepInvalid;if("boolean"!=typeof e)throw new TypeError("keepInvalid() expects a boolean parameter");return p.keepInvalid=e,L},L.datepickerInput=function(e){if(0===arguments.length)return p.datepickerInput;if("string"!=typeof e)throw new TypeError("datepickerInput() expects a string parameter");return p.datepickerInput=e,L},L.parseInputDate=function(e){if(0===arguments.length)return p.parseInputDate;if("function"!=typeof e)throw new TypeError("parseInputDate() sholud be as function");return p.parseInputDate=e,L},L.disabledTimeIntervals=function(e){if(0===arguments.length)return p.disabledTimeIntervals?$.extend({},p.disabledTimeIntervals):p.disabledTimeIntervals;if(!e)return p.disabledTimeIntervals=!1,y(),L;if(!(e instanceof Array))throw new TypeError("disabledTimeIntervals() expects an array parameter");return p.disabledTimeIntervals=e,y(),L},L.disabledHours=function(e){if(0===arguments.length)return p.disabledHours?$.extend({},p.disabledHours):p.disabledHours;if(!e)return p.disabledHours=!1,y(),L;if(!(e instanceof Array))throw new TypeError("disabledHours() expects an array parameter");if(p.disabledHours=O(e),p.enabledHours=!1,p.useCurrent&&!p.keepInvalid){for(var t=0;!m(E,"h");){if(E.add(1,"h"),24===t)throw"Tried 24 times to find a valid date";t++}g(E)}return y(),L},L.enabledHours=function(e){if(0===arguments.length)return p.enabledHours?$.extend({},p.enabledHours):p.enabledHours;if(!e)return p.enabledHours=!1,y(),L;if(!(e instanceof Array))throw new TypeError("enabledHours() expects an array parameter");if(p.enabledHours=O(e),p.disabledHours=!1,p.useCurrent&&!p.keepInvalid){for(var t=0;!m(E,"h");){if(E.add(1,"h"),24===t)throw"Tried 24 times to find a valid date";t++}g(E)}return y(),L},L.viewDate=function(e){if(0===arguments.length)return H.clone();if(!e)return H=E.clone(),L;if(!("string"==typeof e||_.isMoment(e)||e instanceof Date))throw new TypeError("viewDate() parameter must be one of [string, moment or Date]");return H=v(e),r(),L},i.is("input"))I=i;else if(0===(I=i.find(p.datepickerInput)).length)I=i.find("input");else if(!I.is("input"))throw new Error('CSS class "'+p.datepickerInput+'" cannot be applied to non input element');if(i.hasClass("input-group")&&(z=0===i.find(".datepickerbutton").length?i.find(".input-group-addon"):i.find(".datepickerbutton")),!p.inline&&!I.is("input"))throw new Error("Could not initialize DateTimePicker without an input element");return E=c(),H=E.clone(),$.extend(!0,p,(F={},(A=i.is("input")||p.inline?i.data():i.find("input").data()).dateOptions&&A.dateOptions instanceof Object&&(F=$.extend(!0,F,A.dateOptions)),$.each(p,function(e){var t="date"+e.charAt(0).toUpperCase()+e.slice(1);void 0!==A[t]&&(F[e]=A[t])}),F)),L.options(p),P(),I.on({change:M,blur:p.debug?"":b,keydown:x,keyup:T,focus:p.allowInputToggle?D:""}),i.is("input")?I.on({focus:D}):z&&(z.on("click",C),z.on("mousedown",!1)),I.prop("disabled")&&L.disable(),I.is("input")&&0!==I.val().trim().length?g(v(I.val().trim())):p.defaultDate&&void 0===I.attr("placeholder")&&g(p.defaultDate),p.inline&&D(),L}return $.fn.datetimepicker=function(a){a=a||{};var t,n=Array.prototype.slice.call(arguments,1),r=!0;if("object"==typeof a)return this.each(function(){var e,t=$(this);t.data("DateTimePicker")||(e=$.extend(!0,{},$.fn.datetimepicker.defaults,a),t.data("DateTimePicker",i(t,e)))});if("string"==typeof a)return this.each(function(){var e=$(this).data("DateTimePicker");if(!e)throw new Error('bootstrap-datetimepicker("'+a+'") method was called on an element that is not using DateTimePicker');t=e[a].apply(e,n),r=t===e}),r||-1<$.inArray(a,["destroy","hide","show","toggle"])?this:t;throw new TypeError("Invalid arguments for DateTimePicker: "+a)},$.fn.datetimepicker.defaults={timeZone:"",format:!1,dayViewHeaderFormat:"MMMM YYYY",extraFormats:!1,stepping:1,minDate:!1,maxDate:!1,useCurrent:!0,collapse:!0,locale:_.locale(),defaultDate:!1,disabledDates:!1,enabledDates:!1,icons:{time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down",previous:"glyphicon glyphicon-chevron-left",next:"glyphicon glyphicon-chevron-right",today:"glyphicon glyphicon-screenshot",clear:"glyphicon glyphicon-trash",close:"glyphicon glyphicon-remove"},tooltips:{today:"Go to today",clear:"Clear selection",close:"Close the picker",selectMonth:"Select Month",prevMonth:"Previous Month",nextMonth:"Next Month",selectYear:"Select Year",prevYear:"Previous Year",nextYear:"Next Year",selectDecade:"Select Decade",prevDecade:"Previous Decade",nextDecade:"Next Decade",prevCentury:"Previous Century",nextCentury:"Next Century",pickHour:"Pick Hour",incrementHour:"Increment Hour",decrementHour:"Decrement Hour",pickMinute:"Pick Minute",incrementMinute:"Increment Minute",decrementMinute:"Decrement Minute",pickSecond:"Pick Second",incrementSecond:"Increment Second",decrementSecond:"Decrement Second",togglePeriod:"Toggle Period",selectTime:"Select Time"},useStrict:!1,sideBySide:!1,daysOfWeekDisabled:!1,calendarWeeks:!1,viewMode:"days",toolbarPlacement:"default",showTodayButton:!1,showClear:!1,showClose:!1,widgetPositioning:{horizontal:"auto",vertical:"auto"},widgetParent:null,ignoreReadonly:!1,keepOpen:!1,focusOnShow:!0,inline:!1,keepInvalid:!1,datepickerInput:".datepickerinput",keyBinds:{up:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")?this.date(t.clone().subtract(7,"d")):this.date(t.clone().add(this.stepping(),"m"))}},down:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")?this.date(t.clone().add(7,"d")):this.date(t.clone().subtract(this.stepping(),"m"))}else this.show()},"control up":function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")?this.date(t.clone().subtract(1,"y")):this.date(t.clone().add(1,"h"))}},"control down":function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")?this.date(t.clone().add(1,"y")):this.date(t.clone().subtract(1,"h"))}},left:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")&&this.date(t.clone().subtract(1,"d"))}},right:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")&&this.date(t.clone().add(1,"d"))}},pageUp:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")&&this.date(t.clone().subtract(1,"M"))}},pageDown:function(e){if(e){var t=this.date()||this.getMoment();e.find(".datepicker").is(":visible")&&this.date(t.clone().add(1,"M"))}},enter:function(){this.hide()},escape:function(){this.hide()},"control space":function(e){e&&e.find(".timepicker").is(":visible")&&e.find('.btn[data-action="togglePeriod"]').click()},t:function(){this.date(this.getMoment())},delete:function(){this.clear()}},debug:!1,allowInputToggle:!1,disabledTimeIntervals:!1,disabledHours:!1,enabledHours:!1,viewDate:!1},$.fn.datetimepicker});
\ No newline at end of file
diff --git a/javascript/fabric.min.js b/javascript/src/Plugins/fabric.min.js
similarity index 100%
rename from javascript/fabric.min.js
rename to javascript/src/Plugins/fabric.min.js
diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js
index 0a26cb5d6cee905a14460516281f87bd95973989..fae0ac550e00509376f3e34c188b987b99d30fec 100644
--- a/javascript/src/QfqForm.js
+++ b/javascript/src/QfqForm.js
@@ -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";
diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less
index 02f2ca35ef968117d23ba3a2eb0c79ff151acf5a..2f34ff5d29f7c4cb02260a7c63354a3bac013905 100644
--- a/less/qfq-bs.css.less
+++ b/less/qfq-bs.css.less
@@ -327,7 +327,7 @@ i.@{spinner_class} {
   background-repeat: repeat-x;
   border-color: #d30b6f;
   font-weight: 200;
-  text-shadow: 0 1px 0 #e72a8;
+  text-shadow: #e72a89 0 1px 0;
   color: #333;
 }
 
diff --git a/package.json b/package.json
index e3cc91bd7f685c1463ce0ab08e85b7d45f7e8ac6..655410b8f8cae664501a398d230faecf02c67a16 100644
--- a/package.json
+++ b/package.json
@@ -13,31 +13,44 @@
     "filepond-plugin-file-validate-size": "latest",
     "filepond-plugin-image-preview": "latest",
     "filepond-plugin-image-edit": "latest",
+    "concat": "^1.0.3",
     "corejs-typeahead": "^1.3.1",
-    "eonasdan-bootstrap-datetimepicker": "^4.17.49",
     "fullcalendar": "^3.10.2",
-    "grunt": "^1.6.1",
-    "grunt-concat-in-order": "^0.2.6",
-    "grunt-contrib-concat": "^1.0.1",
-    "grunt-contrib-copy": "^1.0.0",
-    "grunt-contrib-jasmine": "^1.1.0",
-    "grunt-contrib-jshint": "^1.1.0",
-    "grunt-contrib-less": "^1.2.0",
-    "grunt-contrib-watch": "^1.0.0",
-    "grunt-terser": "^2.0.0",
     "jquery": "latest",
     "jqwidgets-framework": "4.2.1",
-    "moment": "^2.29.4",
+    "jshint": "^2.13.6",
+    "less": "^4.2.0",
+    "less-plugin-clean-css": "^1.5.1",
+    "moment": "latest",
+    "ncp": "^2.0.0",
     "popper.js": "^1.16.1",
     "selenium-webdriver": "^4.14.0",
     "should": "^11.2.1",
     "tablesorter": "^2.31.3",
-    "terser": "^5.21.0",
+    "terser": "latest",
     "tinymce": "^4.9.11",
     "wolfy87-eventemitter": "^4.3.0"
   },
+  "jshintConfig": {
+    "esversion": 6,
+    "asi": true
+  },
+  "config": {
+    "js-dir": "extension/Resources/Public/JavaScript/",
+    "css-dir": "extension/Resources/Public/Css/",
+    "font-dir": "extension/Resources/Public/fonts/"
+  },
   "scripts": {
-    "test": "mocha tests/selenium/test*.js"
+    "test": "mocha tests/selenium/test*.js",
+    "create-dirs": "mkdir -p js && mkdir -p javascript/build/dist && mkdir -p extension/Resources/Public/JavaScript/ && mkdir -p extension/Resources/Public/Css/ && mkdir -p extension/Resources/Public/fonts/",
+    "copy": "node javascript/build/copy.js",
+    "echo": "echo \"$npm_package_config_js_dir\"",
+    "concat": "concat -o javascript/build/dist/qfq.debug.js javascript/src/Core/QfqEvents.js javascript/src/Core/FormGroup.js javascript/src/*.js javascript/src/Helper/*.js javascript/src/Element/*.js",
+    "terser": "node javascript/build/terser.js",
+    "jshint": "jshint javascript/src --exclude javascript/src/Plugins",
+    "less": "lessc -clean-css less/qfq-bs.css.less less/dist/qfq-bs.css && lessc -clean-css less/qfq-letter.css.less less/dist/qfq-letter.css && lessc -clean-css less/qfq-plain.css.less less/dist/qfq-plain.css && lessc -clean-css less/tablesorter-bootstrap.less less/dist/tablesorter-bootstrap.css",
+    "prebuild": "npm run jshint && npm run create-dirs",
+    "build": "npm run concat && npm run less && npm run terser && npm run copy"
   },
   "license": "ISC",
   "repository": "https://git.math.uzh.ch/typo3/qfq",