2
2
3
3
namespace App \Modifiers ;
4
4
5
+ use Illuminate \Support \Arr ;
5
6
use Statamic \Modifiers \Modifier ;
7
+ use Statamic \Statamic ;
6
8
7
9
class Toc extends Modifier
8
10
{
@@ -11,57 +13,68 @@ class Toc extends Modifier
11
13
/**
12
14
* Modify a value
13
15
*
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
17
19
* @return mixed
18
20
*/
19
21
public function index ($ value , $ params , $ context )
20
22
{
21
23
$ this ->context = $ context ;
22
24
23
- $ creatingIds = array_get ($ params , 0 ) == 'ids ' ;
25
+ $ creatingIds = Arr:: get ($ params , 0 ) == 'ids ' ;
24
26
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 );
26
29
27
30
return $ creatingIds ? $ content : $ toc ;
28
31
}
29
32
30
33
// Good golly this thing is ugly.
31
34
private function create ($ content , $ maxHeadingLevels )
32
35
{
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
+ }
34
43
35
44
if (! $ matches ) {
36
45
return [null , $ content ];
37
46
}
38
47
48
+ // Track unique anchor IDs across the document
39
49
global $ anchors ;
50
+ $ anchors = [];
40
51
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" ;
43
54
$ i = 0 ;
44
-
45
- // Wangjangle params, vars, and options in there.
46
- $ matches = $ this ->appendDetails ($ matches );
55
+ $ tiCounter = 1 ; // Add counter for --ti values
47
56
48
57
foreach ($ matches as $ heading ) {
58
+ // Track the starting heading level for proper list nesting
49
59
if ($ i == 0 ) {
50
- $ startlvl = $ heading [1 ];
60
+ $ startlvl = ( $ heading [ 1 ] == ' 1 ' ) ? ' 2 ' : $ heading [1 ];
51
61
}
52
62
53
- $ lvl = $ heading [1 ];
63
+ // Normalize h1 to same level as h2
64
+ $ lvl = ($ heading [1 ] == '1 ' ) ? '2 ' : $ heading [1 ];
54
65
66
+ // Check if heading already has an ID attribute
55
67
$ ret = preg_match ('/id=[ \'|"](.*)?[ \'|"]/i ' , stripslashes ($ heading [2 ]), $ anchor );
56
68
57
69
if ($ ret && $ anchor [1 ] != '' ) {
58
70
$ anchor = trim (stripslashes ($ anchor [1 ]));
59
71
$ add_id = false ;
60
72
} 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 ]);
62
75
$ add_id = true ;
63
76
}
64
-
77
+ // Ensure anchor ID is unique by adding numeric suffixes if needed
65
78
if (! in_array ($ anchor , $ anchors )) {
66
79
$ anchors [] = $ anchor ;
67
80
} else {
@@ -74,10 +87,12 @@ private function create($content, $maxHeadingLevels)
74
87
$ anchors [] = $ anchor ;
75
88
}
76
89
90
+ // Add ID to the heading in content if it didn't have one
77
91
if ($ add_id ) {
78
92
$ content = substr_replace ($ content , '<h ' .$ lvl .' id=" ' .$ anchor .'" ' .$ heading [2 ].'> ' .$ heading [3 ].'</h ' .$ lvl .'> ' , strpos ($ content , $ heading [0 ]), strlen ($ heading [0 ]));
79
93
}
80
94
95
+ // Extract title from title attribute or use heading text
81
96
$ ret = preg_match ('/title=[ \'|"](.*)?[ \'|"]/i ' , stripslashes ($ heading [2 ]), $ title );
82
97
83
98
if ($ ret && $ title [1 ] != '' ) {
@@ -88,22 +103,28 @@ private function create($content, $maxHeadingLevels)
88
103
89
104
$ title = trim (strip_tags ($ title ));
90
105
106
+ // Handle nested list structure based on heading levels
91
107
if ($ i > 0 ) {
92
108
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" ;
94
111
} elseif ($ prevlvl > $ lvl ) {
112
+ // Close current item and any nested lists
95
113
$ toc .= '</li> ' ."\n" ;
96
114
while ($ prevlvl > $ lvl ) {
97
- $ toc .= " </ol> " ."\n" .'</li> ' ."\n" ;
115
+ $ toc .= ' </ul></li> ' ."\n" .'</li> ' ."\n" ;
98
116
$ prevlvl --;
99
117
}
100
118
} else {
119
+ // Close current item at same level
101
120
$ toc .= '</li> ' ."\n" ;
102
121
}
103
122
}
104
123
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
+
107
128
$ prevlvl = $ lvl ;
108
129
109
130
$ i ++;
@@ -112,19 +133,19 @@ private function create($content, $maxHeadingLevels)
112
133
unset($ anchors );
113
134
114
135
while ($ lvl > $ startlvl ) {
115
- $ toc .= "\n</ol > " ;
136
+ $ toc .= "\n</ul > " ;
116
137
$ lvl --;
117
138
}
118
139
119
140
$ 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" ;
124
142
125
143
return [$ toc , $ content ];
126
144
}
127
145
146
+ /**
147
+ * Safely extracts value from Statamic Value objects
148
+ */
128
149
private function valueGet ($ value )
129
150
{
130
151
if ($ value instanceof \Statamic \Fields \Value) {
@@ -134,41 +155,10 @@ private function valueGet($value)
134
155
return $ value ;
135
156
}
136
157
137
- private function appendDetails ( $ matches )
158
+ private function slugify ( $ text )
138
159
{
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 );
173
163
}
174
164
}
0 commit comments