22
33namespace App \Modifiers ;
44
5+ use Illuminate \Support \Arr ;
56use Statamic \Modifiers \Modifier ;
7+ use Statamic \Statamic ;
68
79class Toc extends Modifier
810{
@@ -11,57 +13,68 @@ class Toc extends Modifier
1113 /**
1214 * Modify a value
1315 *
14- * @param mixed $value The value to be modified
15- * @param array $params Any parameters used in the modifier
16- * @param array $context Contextual values
16+ * @param mixed $value The value to be modified
17+ * @param array $params Any parameters used in the modifier
18+ * @param array $context Contextual values
1719 * @return mixed
1820 */
1921 public function index ($ value , $ params , $ context )
2022 {
2123 $ this ->context = $ context ;
2224
23- $ creatingIds = array_get ($ params , 0 ) == 'ids ' ;
25+ $ creatingIds = Arr:: get ($ params , 0 ) == 'ids ' ;
2426
25- list ($ toc , $ content ) = $ this ->create ($ value , $ creatingIds ? 5 : 3 );
27+ // Here maxHeadingLevels is set to either 5 (when creating IDs) or 3 (for TOC)
28+ [$ toc , $ content ] = $ this ->create ($ value , $ creatingIds ? 5 : 3 );
2629
2730 return $ creatingIds ? $ content : $ toc ;
2831 }
2932
3033 // Good golly this thing is ugly.
3134 private function create ($ content , $ maxHeadingLevels )
3235 {
33- preg_match_all ('/<h([1- ' .$ maxHeadingLevels .'])([^>]*)>(.*)<\/h[1- ' .$ maxHeadingLevels .']>/i ' , $ content , $ matches , PREG_SET_ORDER );
36+ // First try with h2-hN headings
37+ preg_match_all ('/<h([2- ' .$ maxHeadingLevels .'])([^>]*)>(.*)<\/h[2- ' .$ maxHeadingLevels .']>/i ' , $ content , $ matches , PREG_SET_ORDER );
38+
39+ // If we don't have enough entries, include h1 headings as well
40+ if (count ($ matches ) < 3 ) {
41+ preg_match_all ('/<h([1- ' .$ maxHeadingLevels .'])([^>]*)>(.*)<\/h[1- ' .$ maxHeadingLevels .']>/i ' , $ content , $ matches , PREG_SET_ORDER );
42+ }
3443
3544 if (! $ matches ) {
3645 return [null , $ content ];
3746 }
3847
48+ // Track unique anchor IDs across the document
3949 global $ anchors ;
50+ $ anchors = [];
4051
41- $ anchors = array ();
42- $ toc = '<ol class="toc"> ' ."\n" ;
52+ // Initialize TOC with an unordered list
53+ $ toc = '<ul class="o-scroll-spy-timeline__toc js__scroll-spy- toc"> ' ."\n" ;
4354 $ i = 0 ;
44-
45- // Wangjangle params, vars, and options in there.
46- $ matches = $ this ->appendDetails ($ matches );
55+ $ tiCounter = 1 ; // Add counter for --ti values
4756
4857 foreach ($ matches as $ heading ) {
58+ // Track the starting heading level for proper list nesting
4959 if ($ i == 0 ) {
50- $ startlvl = $ heading [1 ];
60+ $ startlvl = ( $ heading [ 1 ] == ' 1 ' ) ? ' 2 ' : $ heading [1 ];
5161 }
5262
53- $ lvl = $ heading [1 ];
63+ // Normalize h1 to same level as h2
64+ $ lvl = ($ heading [1 ] == '1 ' ) ? '2 ' : $ heading [1 ];
5465
66+ // Check if heading already has an ID attribute
5567 $ ret = preg_match ('/id=[ \'|"](.*)?[ \'|"]/i ' , stripslashes ($ heading [2 ]), $ anchor );
5668
5769 if ($ ret && $ anchor [1 ] != '' ) {
5870 $ anchor = trim (stripslashes ($ anchor [1 ]));
5971 $ add_id = false ;
6072 } else {
61- $ anchor = preg_replace ('/\s+/ ' , '- ' , trim (preg_replace ('/[^a-z\s]/ ' , '' , strtolower (strip_tags ($ heading [3 ])))));
73+ // Generate an ID from the heading text
74+ $ anchor = $ this ->slugify ($ heading [3 ]);
6275 $ add_id = true ;
6376 }
64-
77+ // Ensure anchor ID is unique by adding numeric suffixes if needed
6578 if (! in_array ($ anchor , $ anchors )) {
6679 $ anchors [] = $ anchor ;
6780 } else {
@@ -74,10 +87,12 @@ private function create($content, $maxHeadingLevels)
7487 $ anchors [] = $ anchor ;
7588 }
7689
90+ // Add ID to the heading in content if it didn't have one
7791 if ($ add_id ) {
7892 $ content = substr_replace ($ content , '<h ' .$ lvl .' id=" ' .$ anchor .'" ' .$ heading [2 ].'> ' .$ heading [3 ].'</h ' .$ lvl .'> ' , strpos ($ content , $ heading [0 ]), strlen ($ heading [0 ]));
7993 }
8094
95+ // Extract title from title attribute or use heading text
8196 $ ret = preg_match ('/title=[ \'|"](.*)?[ \'|"]/i ' , stripslashes ($ heading [2 ]), $ title );
8297
8398 if ($ ret && $ title [1 ] != '' ) {
@@ -88,22 +103,28 @@ private function create($content, $maxHeadingLevels)
88103
89104 $ title = trim (strip_tags ($ title ));
90105
106+ // Handle nested list structure based on heading levels
91107 if ($ i > 0 ) {
92108 if ($ prevlvl < $ lvl ) {
93- $ toc .= "\n" ."<ol> " ."\n" ;
109+ // Start a new nested list wrapped in li, don't increment counter for parent li
110+ $ toc .= "\n" .'<li><ul> ' ."\n" ;
94111 } elseif ($ prevlvl > $ lvl ) {
112+ // Close current item and any nested lists
95113 $ toc .= '</li> ' ."\n" ;
96114 while ($ prevlvl > $ lvl ) {
97- $ toc .= " </ol> " ."\n" .'</li> ' ."\n" ;
115+ $ toc .= ' </ul></li> ' ."\n" .'</li> ' ."\n" ;
98116 $ prevlvl --;
99117 }
100118 } else {
119+ // Close current item at same level
101120 $ toc .= '</li> ' ."\n" ;
102121 }
103122 }
104123
105- $ j = 0 ;
106- $ toc .= '<li><a href="# ' .$ anchor .'"> ' .$ title .'</a> ' ;
124+ // Add TOC entry with --ti style (only for leaf nodes)
125+ $ toc .= '<li style="--ti: -- ' .$ tiCounter .'"><a href="# ' .$ anchor .'"> ' .$ title .'</a> ' ;
126+ $ tiCounter ++;
127+
107128 $ prevlvl = $ lvl ;
108129
109130 $ i ++;
@@ -112,19 +133,19 @@ private function create($content, $maxHeadingLevels)
112133 unset($ anchors );
113134
114135 while ($ lvl > $ startlvl ) {
115- $ toc .= "\n</ol > " ;
136+ $ toc .= "\n</ul > " ;
116137 $ lvl --;
117138 }
118139
119140 $ toc .= '</li> ' ."\n" ;
120- $ toc .= '</ol> ' ."\n" ;
121-
122- // A tiny TOC is a lame TOC
123- $ toc = (count ($ matches ) < 3 ) ? null : $ toc ;
141+ $ toc .= '</ul> ' ."\n" ;
124142
125143 return [$ toc , $ content ];
126144 }
127145
146+ /**
147+ * Safely extracts value from Statamic Value objects
148+ */
128149 private function valueGet ($ value )
129150 {
130151 if ($ value instanceof \Statamic \Fields \Value) {
@@ -134,41 +155,10 @@ private function valueGet($value)
134155 return $ value ;
135156 }
136157
137- private function appendDetails ( $ matches )
158+ private function slugify ( $ text )
138159 {
139- $ parameters = $ this ->valueGet ($ this ->context ['parameters ' ] ?? null );
140-
141- if ($ parameters && count ($ parameters ) > 0 ) {
142- $ matches [] = [
143- '<h2 id="parameters">Parameters</h2> ' ,
144- '2 ' ,
145- ' id="parameters" ' ,
146- 'Parameters '
147- ];
148- }
149-
150- $ variables = $ this ->valueGet ($ this ->context ['variables ' ] ?? null );
151-
152- if ($ variables && count ($ variables ) > 0 ) {
153- $ matches [] = [
154- '<h2 id="variables">Variables</h2> ' ,
155- '2 ' ,
156- ' id="variables" ' ,
157- 'Variables '
158- ];
159- }
160-
161- $ options = $ this ->valueGet ($ this ->context ['options ' ] ?? null );
162-
163- if ($ options && count ($ options ) > 0 ) {
164- $ matches [] = [
165- '<h2 id="options">Options</h2> ' ,
166- '2 ' ,
167- ' id="options" ' ,
168- 'Options '
169- ];
170- }
171-
172- return $ matches ;
160+ $ slugified = Statamic::modify ($ text )->replace ('& ' , '' )->slugify ()->stripTags ();
161+ // Remove 'code-code' from the slugified text e.g. Otherwise "the `@` ignore symbol" gets converted to `the-code-code-ignore-symbol`
162+ return str_replace ('code-code- ' , '' , $ slugified );
173163 }
174164}
0 commit comments