From f128373fa309eef562b6f175f8846029e1a9fac9 Mon Sep 17 00:00:00 2001
From: jhaller <jan.haller@math.uzh.ch>
Date: Fri, 7 Jul 2023 10:40:19 +0200
Subject: [PATCH] F8975: Added documentation for report notation 2.0. refs
 #8975

---
 Documentation/Concept.rst                     | 13 ++--
 Documentation/Report.rst                      | 69 ++++++++++++++++++-
 Documentation/Variable.rst                    |  4 +-
 extension/Classes/Core/BodytextParser.php     |  9 ++-
 extension/Classes/Core/Constants.php          |  1 -
 .../Tests/Unit/Core/BodytextParserTest.php    |  6 +-
 6 files changed, 84 insertions(+), 18 deletions(-)

diff --git a/Documentation/Concept.rst b/Documentation/Concept.rst
index 7d3bf589c..aabbc5431 100644
--- a/Documentation/Concept.rst
+++ b/Documentation/Concept.rst
@@ -165,21 +165,26 @@ QFQ Keywords (Bodytext)
 |                         | | 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.                                 |
+| <alias>.line.count      | ``<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>.line.total      | Total rows (MySQL ``num_rows`` for *SELECT* and *SHOW*, MySQL ``affected_rows`` |
-|                         | for *UPDATE* and *INSERT*.                                                      |
+| <alias>.line.total      | for *UPDATE* and *INSERT*.                                                      |
 +-------------------------+---------------------------------------------------------------------------------+
 | <level>.line.insertId   | Last insert id for *INSERT*.                                                    |
+| <alias>.line.insertId   |                                                                                 |
 +-------------------------+---------------------------------------------------------------------------------+
-| <level>.line.content    | Show content of `<level>` (content have to be stored via <level>.content=....)  |
+| <level>.line.content    | Show content of `<level>`/`<alias>` (content have to be stored via              |
+| <alias>.line.content    | <level>.content=... or <alias>.content=...).                                    |
 +-------------------------+---------------------------------------------------------------------------------+
 | <level>.line.altCount   | Like 'line.count' but for 'alt' query.                                          |
+| <alias>.line.altCount   |                                                                                 |
 +-------------------------+---------------------------------------------------------------------------------+
 | <level>.line.altTotal   | Like 'line.total' but for 'alt' query.                                          |
+| <alias>.line.altTotal   |                                                                                 |
 +-------------------------+---------------------------------------------------------------------------------+
 | <level>.line.altInsertId| Like 'line.insertId' but for 'alt' query.                                       |
+| <alias>.line.altInsertId|                                                                                 |
 +-------------------------+---------------------------------------------------------------------------------+
 
 .. _`report-render`:
diff --git a/Documentation/Report.rst b/Documentation/Report.rst
index 224572221..ed2ca256a 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 (version 1)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Levels can be nested. E.g.::
 
@@ -417,6 +417,56 @@ This is equal to::
   10.5.sql = SELECT ...
   10.5.head = ...
 
+Nesting of levels (version 2)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+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 ...
+    myAlias2 {
+        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 version 1 or 2 is used. Using an alias or no level triggers report notation version 2.
+It requires the use of delimiters throughout the report. A combination with the notation '10.sql = ...' is not possible.
+
+.. important::
+
+Report notation version 2 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
 ^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -528,7 +578,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:
 
 +-------------+------------------------------------------------------------------------------------------------------------------------+
@@ -542,6 +605,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.                                                    |
diff --git a/Documentation/Variable.rst b/Documentation/Variable.rst
index 0f58273e6..7e3e10c01 100644
--- a/Documentation/Variable.rst
+++ b/Documentation/Variable.rst
@@ -404,11 +404,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/extension/Classes/Core/BodytextParser.php b/extension/Classes/Core/BodytextParser.php
index 84e15d138..73dc4daa4 100644
--- a/extension/Classes/Core/BodytextParser.php
+++ b/extension/Classes/Core/BodytextParser.php
@@ -218,7 +218,7 @@ class BodytextParser {
                 // 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 disregarded
-                if (empty($firstToken) && 1 !== preg_match('/^(' . TOKEN_VALID_LIST . ')\s*=/', $row)) {
+                if (empty($firstToken) && 1 !== preg_match('/^(' . TOKEN_VALID_LIST . ')\s*=/', $row) && $row !== $nestingOpen && $row !== $nestingClose) {
                     $firstToken = (strpos($row, $nestingOpen) !== false) ? trim(substr($row, 0, strpos($row, $nestingOpen))) : $row;
                 }
 
@@ -399,10 +399,9 @@ class BodytextParser {
                 // $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, contains whitespace, $alias is empty
-                // E.g. no alias or number is used: $level = "1.sql = SELECT ..."
-                // E.g. alias is used: $level = "myAlias"
-                $alias = (strpos($level, ' ')) ? '' : $level;
+                // If the $level, from which the $alias is extracted, nothing gets saved
+                // Allowed characters: [a-zA-Z0-9]
+                $alias = (1 === preg_match('/^[a-zA-Z0-9]+$/', $level) || $level === '') ? $level : null;
 
                 // If no alias is set, then nothing gets saved
                 if (!empty($alias)) {
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index ee0222b7f..496c84bab 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -257,7 +257,6 @@ const ERROR_INVALID_SAVE_ZIP_FILENAME = 1413;
 const ERROR_NUMERIC_ALIAS = 1414;
 const ERROR_INVALID_LEVEL = 1415;
 
-
 // Upload
 const ERROR_UPLOAD = 1500;
 const ERROR_UPLOAD_TOO_BIG = 1501;
diff --git a/extension/Tests/Unit/Core/BodytextParserTest.php b/extension/Tests/Unit/Core/BodytextParserTest.php
index f5c6779d1..4d5780b74 100644
--- a/extension/Tests/Unit/Core/BodytextParserTest.php
+++ b/extension/Tests/Unit/Core/BodytextParserTest.php
@@ -785,9 +785,8 @@ EOF;
                         LIMIT 4
                    head = <div>
                 $close
-                1.tail = </div>
 EOF;
-            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>\n1.tail = </div>";
+            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>";
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
@@ -804,9 +803,8 @@ EOF;
                         LIMIT 4
                    head = <div>
                 $close
-                1.tail = </div>
 EOF;
-            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>\n1.tail = </div>";
+            $expected = "1.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n1.head = <div>";
             $result = $btp->process($given);
             $this->assertEquals($expected, $result);
 
-- 
GitLab