[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX][ENH] enhance parserlib heading and autonumbering

"Victor Emanouilov \(@kroky\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69cd047635594_3b18fd9476dd@gitlab-sidekiq-low-urgency-cpu-bound-v2-999554d69-4str5.mail>

Victor Emanouilov pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
739229c2 by Sammy Ndabo at 2026-04-01T11:34:18+00:00
[FIX][ENH] enhance parserlib heading and autonumbering
---
* [FIX] parserlib: preserve legacy heading spacing for unnumbered headings

* [FIX,ENH] parselib.php: restore numbered-heading tests and parser ids

* [FIX] formatting of heading in ParseToWysiwyg_TextTest to ensure consistent spacing

* [REM] parselib.php: Remove unused variables in ParserLib to clean up code

* Fix heading IDs in WikiParser tests for collapsible sections and ensure consistent formatting

* Enhance tests for numbered headings and improve parsing logic for complex heading structures

* Fix heading when there is no numbering

* [REF] Refactor and Remove multilevel header numbering preference and clean up related tests

* Update pref name and the reset numbering symbol

* [ENH] Parserlib: update test to expect multilevel headings only when the pref that enable it is enabled

* [ENH] parselib.php: cleaned and reviewed code

* [ENH] Wiki parser: Add ^# syntax to reset heading numbering and ensures proper sequence continuation after numbering reset

* Enhance parselib heading and autonumbering

See merge request tikiwiki/tiki!7609

- - - - -


2 changed files:

- lib/parser/parserlib.php
- lib/test/editlib/ParseToWysiwyg_TextTest.php


Changes:

=====================================
lib/parser/parserlib.php
=====================================
@@ -2530,11 +2530,9 @@ class ParserLib extends TikiDb_Bridge
         $data = '';
         $listbeg = [];
         $divdepth = [];
-        $hdr_structure = [];
-        $show_title_level = [];
-        $last_hdr = [];
+        $numbering = [];
+        $counters = [];
         $all_anchors = [];
-        $nb_last_hdr = 0;
         $nb_hdrs = 0;
         $nb_lists = 0;
         $inTable = 0;
@@ -2548,6 +2546,16 @@ class ParserLib extends TikiDb_Bridge
         $in_paragraph = 0;
         $in_empty_paragraph = 0;
 
+        $min_level = null;
+        foreach ($lines as $line) {
+            if (preg_match('/^(!+)[\+\-]?#/', $line, $m)) {
+                $level = strlen($m[1]);
+                if ($min_level === null || $level < $min_level) {
+                    $min_level = $level;
+                }
+            }
+        }
+
         foreach ($lines as $line) {
             // Add newlines between lines
             if (isset($current_title_num)) { // Exclude the first line
@@ -2755,76 +2763,56 @@ class ParserLib extends TikiDb_Bridge
                     $hdrlevel = $tikilib->how_many_at_start($line, '!');
 
                     if ($litype == '!' && $hdrlevel > 0 && $hdrlevel <= 6 /* HTML has h1 to h6, but no h7 or above */) { // If the line starts with 1 to 6 exclamation marks ("!")
+                        ++$nb_hdrs;
+                        if (preg_match('/^!+[\+\-]?#\)/', $line)) {
+                            $counters = [];
+                            $line = preg_replace('/(^!+[\+\-]?)(#)\)/', '$1$2', $line);
+                        }
+
+                        $level = $hdrlevel;
+                        $current_title_real_num = '';
+                        $current_title_num = '';
+
                         /*
                          * Handle headings autonumbering syntax (i.e. !#Text, !!#Text, ...)
-                         * Note :
-                         *    this needs to be done even if the current header has no '#'
-                         *    in order to generate the right numbers when they are not specified for every headers.
-                         *    This is the case, for example, when you want to add numbers to headers of level 2 but not to level 1
+                         * Note:
+                         *     this needs to be done even if the current header has no '#'
+                         *     in order to generate the right numbers when they are not specified for every headers.
+                         *     This is the case, for example, when you want to add numbers to headers of level 2 but not to level 1.
                          */
-
-                        $line_lenght = strlen($line);
-
-                        // Generate an array containing the squeleton of maketoc (based on headers levels)
-                        //   i.e. hdr_structure will contain something lile this :
-                        //     array( 1, 2, 2.1, 2.1.1, 2.1.2, 2.2, ... , X.Y.Z... )
-                        //
-
-                        $hdr_structure[$nb_hdrs] = [];
-
-                        // Generate the number (e.g. 1.2.1.1) of the current title, based on the previous title number :
-                        //   - if the current title deepest level is lesser than (or equal to)
-                        //     the deepest level of the previous title : then we increment the last level number,
-                        //   - else : we simply add new levels with value '1' (only if the previous level number was shown),
-                        //
-                        if ($nb_last_hdr > 0 && $hdrlevel <= $nb_last_hdr) {
-                            $hdr_structure[$nb_hdrs] = array_slice($last_hdr, 0, $hdrlevel);
-                            if (! empty($show_title_level[$hdrlevel]) || ! $need_autonumbering) {
-                                //
-                                // Increment the level number only if :
-                                //     - the last title of the same level number has a displayed number
-                                //  or - no title has a displayed number (no autonumbering)
-                                //
-                                $hdr_structure[$nb_hdrs][$hdrlevel - 1]++;
-                            }
-                        } else {
-                            if ($nb_last_hdr > 0) {
-                                $hdr_structure[$nb_hdrs] = $last_hdr;
-                            }
-                            for ($h = 0; $h < $hdrlevel - $nb_last_hdr; $h++) {
-                                $hdr_structure[$nb_hdrs][$h + $nb_last_hdr] = '1';
+                        $support_numbering = preg_match('/^!+[\+\-]?#/', $line);
+                        if ($support_numbering) {
+                            // Normalize level (so the lowest becomes level 1)
+                            $relative_level = $level - $min_level + 1;
+
+                            // Reset deeper levels
+                            for ($i = $relative_level + 1; $i <= 10; $i++) {
+                                unset($counters[$i]);
                             }
-                        }
-                        $show_title_level[$hdrlevel] = preg_match('/^!+[\+\-]?#/', $line);
 
-                        // Update last_hdr info for the next header
-                        $last_hdr = $hdr_structure[$nb_hdrs];
-                        $nb_last_hdr = count($last_hdr);
+                            // Increment this level
+                            if (! isset($counters[$relative_level])) {
+                                $counters[$relative_level] = 1;
+                            } else {
+                                $counters[$relative_level]++;
+                            }
 
-                        if (is_array($last_hdr)) {
-                            $current_title_real_num = implode('.', $last_hdr) . '. ';
-                        } else {
-                            $current_title_real_num = $last_hdr . '. ';
-                        }
+                            // Ensure parent levels exist (initialize with 1 if missing)
+                            for ($i = 1; $i < $relative_level; $i++) {
+                                if (! isset($counters[$i])) {
+                                    $counters[$i] = 1;
+                                }
+                            }
 
-                        // Update the current title number to hide all parents levels numbers if the parent has no autonumbering
-                        $hideall = false;
-                        for ($j = $hdrlevel; $j > 0; $j--) {
-                            if ($hideall || empty($show_title_level[$j])) {
-                                unset($hdr_structure[$j - 1]);
-                                $hideall = true;
+                            $numbering = [];
+                            for ($i = 1; $i <= $relative_level; $i++) {
+                                $numbering[] = $counters[$i];
                             }
-                        }
 
-                        // Store the title number to use only if it has to be shown (if the '#' char is used)
-                        $current_title_num = '';
-                        if (isset($show_title_level[$hdrlevel]) && isset($hdr_structure[$nb_hdrs])) {
-                            $current_title_num = $show_title_level[$hdrlevel] ? implode('.', $hdr_structure[$nb_hdrs]) . '. ' : '';
+                            $current_title_num = implode('.', $numbering) . ".";
+                            $current_title_real_num = $current_title_num . ' ';
                         }
 
-                        $nb_hdrs++;
-
-
                         // Close open paragraph (lists already closed above)
                         $this->close_blocks($data, $in_paragraph, $listbeg, $divdepth, 1, 0, 0);
                         // Close lower level divs if opened
@@ -2842,7 +2830,10 @@ class ParserLib extends TikiDb_Bridge
                         $anchor = '';
                         $aclose = '';
                         $aclose2 = '';
-                        $addremove = $show_title_level[$hdrlevel] ? 1 : 0; // If needed, also remove '#' sign from title beginning
+                        $addremove = 0;
+                        if ($support_numbering) {
+                            $addremove = 1; // If needed, also remove '#' sign from title beginning
+                        }
 
                         // May be special signs present after '!'s?
                         $divstate = substr($line, $hdrlevel, 1);
@@ -2865,14 +2856,16 @@ class ParserLib extends TikiDb_Bridge
                         }
 
                         // Generate the final title text
-                        $title_text_base = substr($line, $hdrlevel + $addremove);
-                        $title_text = $current_title_num . $title_text_base;
+                        $title_text_base_raw = substr($line, $hdrlevel + $addremove);
+                        $title_text_base = $support_numbering ? ltrim($title_text_base_raw) : $title_text_base_raw;
+                        $title_text = $current_title_real_num . $title_text_base;
 
                         // Remove HTML tags from Tiki syntax
                         $title = preg_replace('#\<(.*?)\>#', '', $title_text);
                         $thisid = $this->getCleanAnchor($title, $all_anchors);
 
-                        // Collect TOC entry if any {maketoc} is present on the page
+                        // Collect TOC entry if any {maketoc} is present on the page.
+                        // Generates the skeleton of the TOC (e.g. 1, 2, 2.1, 2.1.1, ...).
                         //if ( $need_maketoc !== false ) {
                         $anch[] = [
                                         'id' => $thisid,


=====================================
lib/test/editlib/ParseToWysiwyg_TextTest.php
=====================================
@@ -185,6 +185,7 @@ class EditLib_ParseToWysiwyg_TextTest extends TikiTestCase
      */
     public function testNumberedHeadings(): void
     {
+        // Legacy expectations to keep backward compatibility intact.
         $inData = '!#Heading Level 1';
         $ex = '<h1 class="showhide_heading d-flex justify-content-start" id="Heading_Level_1">1. Heading Level&nbsp;1</h1>';
         $out = trim($this->el->parseToWysiwyg($inData));
@@ -224,6 +225,92 @@ class EditLib_ParseToWysiwyg_TextTest extends TikiTestCase
         $ex .= '<h6 class="showhide_heading d-flex justify-content-start" id="Heading_Level_6">1.1.1.1.1.1. Heading Level&nbsp;6</h6>';
         $out = trim($this->el->parseToWysiwyg($inData));
         $this->assertEquals($ex, $out);
+
+        // Additional coverage validating extended numbering behavior.
+        $inData = <<<TEST
+!# title 1
+!!# title 1.1
+!!!# title 1.2
+!!!!# title 1.3
+!!!!!# title 1.4
+!!!!!!# title 1.4
+!# title 2
+!!# title 2.1
+TEST;
+        $ex = <<<EX
+<h1 class="showhide_heading d-flex justify-content-start" id="title_1">1. title&nbsp;1</h1>
+<h2 class="showhide_heading d-flex justify-content-start" id="title_1.1">1.1. title&nbsp;1.1</h2>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_1.2">1.1.1. title&nbsp;1.2</h3>
+<h4 class="showhide_heading d-flex justify-content-start" id="title_1.3">1.1.1.1. title&nbsp;1.3</h4>
+<h5 class="showhide_heading d-flex justify-content-start" id="title_1.4">1.1.1.1.1. title&nbsp;1.4</h5>
+<h6 class="showhide_heading d-flex justify-content-start" id="title_1.4_2">1.1.1.1.1.1. title&nbsp;1.4</h6>
+<h1 class="showhide_heading d-flex justify-content-start" id="title_2">2. title&nbsp;2</h1>
+<h2 class="showhide_heading d-flex justify-content-start" id="title_2.1">2.1. title&nbsp;2.1</h2>
+EX;
+        $out = trim($this->el->parseToWysiwyg($inData));
+        $this->assertEquals($ex, $out);
+
+        // Any highest level heading should have a number 1
+        $inData = <<<TEST
+!!!# title 1
+!!!!# title 1.1
+!!!!# title 1.2
+!!!# title 2
+!!!# title 2.1
+TEST;
+        $ex = <<<EX
+<h3 class="showhide_heading d-flex justify-content-start" id="title_1">1. title&nbsp;1</h3>
+<h4 class="showhide_heading d-flex justify-content-start" id="title_1.1">1.1. title&nbsp;1.1</h4>
+<h4 class="showhide_heading d-flex justify-content-start" id="title_1.2">1.2. title&nbsp;1.2</h4>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_2">2. title&nbsp;2</h3>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_2.1">3. title&nbsp;2.1</h3>
+EX;
+        $out = trim($this->el->parseToWysiwyg($inData));
+        $this->assertEquals($ex, $out);
+
+        // Heading reset so it starts from 1
+        $inData = <<<TEST
+!!!# title 1
+!!!# title 2
+!!!#) title 1
+!!!# title 2
+TEST;
+        $ex = <<<EX
+<h3 class="showhide_heading d-flex justify-content-start" id="title_1">1. title&nbsp;1</h3>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_2">2. title&nbsp;2</h3>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_1_2">1. title&nbsp;1</h3>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_2_2">2. title&nbsp;2</h3>
+EX;
+        $out = trim($this->el->parseToWysiwyg($inData));
+        $this->assertEquals($ex, $out);
+
+        // More complex example combining numbered and unnumbered headings
+        $inData = <<<TEST
+! Unnumbered Heading
+!!# title 1
+!!!# title 1.1
+!!!!# title 1.2
+!!# title 2
+!!!# title 2
+
+!!!#) Title 1
+!!!# Title 2
+!!!!!# Title 1.2.1.1.
+TEST;
+        $ex = <<<EX
+<h1 class="showhide_heading d-flex justify-content-start" id="Unnumbered_Heading"> Unnumbered Heading</h1>
+<h2 class="showhide_heading d-flex justify-content-start" id="title_1">1. title&nbsp;1</h2>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_1.1">1.1. title&nbsp;1.1</h3>
+<h4 class="showhide_heading d-flex justify-content-start" id="title_1.2">1.1.1. title&nbsp;1.2</h4>
+<h2 class="showhide_heading d-flex justify-content-start" id="title_2">2. title&nbsp;2</h2>
+<h3 class="showhide_heading d-flex justify-content-start" id="title_2_2">2.1. title&nbsp;2</h3>
+<br />
+<h3 class="showhide_heading d-flex justify-content-start" id="Title_1">1.1. Title&nbsp;1</h3>
+<h3 class="showhide_heading d-flex justify-content-start" id="Title_2">1.2. Title&nbsp;2</h3>
+<h5 class="showhide_heading d-flex justify-content-start" id="Title_1.2.1.1.">1.2.1.1. Title&nbsp;1.2.1.1.</h5>
+EX;
+        $out = trim($this->el->parseToWysiwyg($inData));
+        $this->assertEquals($ex, $out);
     }
 
 



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/739229c27c805c273e9534676083157c3b8ab88e

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/739229c27c805c273e9534676083157c3b8ab88e
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.