1 | <?php␊ |
2 | ␊ |
3 | namespace eNTiDi\Autotoc;␊ |
4 | ␊ |
5 | use eNTiDi\Autotoc\Hacks;␊ |
6 | use SilverStripe\Core\Config\Config;␊ |
7 | use SilverStripe\Core\Injector\Injector;␊ |
8 | use SilverStripe\ORM\ArrayList;␊ |
9 | use SilverStripe\ORM\DataExtension;␊ |
10 | use SilverStripe\View\ArrayData;␊ |
11 | use SplObjectStorage;␊ |
12 | ␊ |
13 | class Autotoc extends DataExtension␊ |
14 | {␊ |
15 | /**␊ |
16 | * @config␊ |
17 | * Callable to be used for augmenting a DOMElement: specify as a␊ |
18 | * string in the format "class::method". `Tocifier::prependAnchor`␊ |
19 | * and `Tocifier::setId` are two valid callbacks.␊ |
20 | */␊ |
21 | private static $augment_callback;␊ |
22 | ␊ |
23 | protected static $tocifiers;␊ |
24 | ␊ |
25 | ␊ |
26 | /**␊ |
27 | * Initialize the Autotoc extension.␊ |
28 | *␊ |
29 | * Creates an internal SplObjectStorage where caching the table of␊ |
30 | * contents.␊ |
31 | */␊ |
32 | public function __construct()␊ |
33 | {␊ |
34 | parent::__construct();␊ |
35 | if (empty(self::$tocifiers)) {␊ |
36 | self::$tocifiers = new SplObjectStorage();␊ |
37 | }␊ |
38 | }␊ |
39 | ␊ |
40 | private static function convertNode($node)␊ |
41 | {␊ |
42 | $data = new ArrayData([␊ |
43 | 'Id' => $node['id'],␊ |
44 | 'Title' => $node['title']␊ |
45 | ]);␊ |
46 | ␊ |
47 | if (isset($node['children'])) {␊ |
48 | $data->setField('Children', self::convertChildren($node['children']));␊ |
49 | }␊ |
50 | ␊ |
51 | return $data;␊ |
52 | }␊ |
53 | ␊ |
54 | private static function convertChildren($children)␊ |
55 | {␊ |
56 | $list = new ArrayList();␊ |
57 | ␊ |
58 | foreach ($children as $child) {␊ |
59 | $list->push(self::convertNode($child));␊ |
60 | }␊ |
61 | ␊ |
62 | return $list;␊ |
63 | }␊ |
64 | ␊ |
65 | /**␊ |
66 | * Get the field name to be used as content.␊ |
67 | * @return string␊ |
68 | */␊ |
69 | private function contentField()␊ |
70 | {␊ |
71 | $field = $this->owner->config()->get('content_field');␊ |
72 | return $field ? $field : 'Content';␊ |
73 | }␊ |
74 | ␊ |
75 | /**␊ |
76 | * Provide content_field customization on a class basis.␊ |
77 | *␊ |
78 | * Override the default setOwner() method so, when valorized, I can␊ |
79 | * enhance the (possibly custom) content field with anchors. I did␊ |
80 | * not find a better way to override a field other than directly␊ |
81 | * substituting it with setField().␊ |
82 | *␊ |
83 | * @param Object $owner␊ |
84 | */␊ |
85 | public function setOwner($owner)␊ |
86 | {␊ |
87 | parent::setOwner($owner);␊ |
88 | if ($owner) {␊ |
89 | Hacks::addCallbackMethodToInstance(␊ |
90 | $owner,␊ |
91 | 'get'.$this->contentField(),␊ |
92 | function() use ($owner) {␊ |
93 | return $owner->getContentField();␊ |
94 | }␊ |
95 | );␊ |
96 | }␊ |
97 | }␊ |
98 | ␊ |
99 | /**␊ |
100 | * Return the internal Tocifier instance bound to $owner.␊ |
101 | *␊ |
102 | * If not present, try to create and execute a new one. On failure␊ |
103 | * (e.g. because of malformed content) no further attempts will be␊ |
104 | * made.␊ |
105 | *␊ |
106 | * @param \SilverStripe\ORM\DataObject $owner␊ |
107 | * @return Tocifier|false|null␊ |
108 | */␊ |
109 | private static function getTocifier($owner)␊ |
110 | {␊ |
111 | if (!$owner) {␊ |
112 | $tocifier = null;␊ |
113 | } elseif (isset(self::$tocifiers[$owner])) {␊ |
114 | $tocifier = self::$tocifiers[$owner];␊ |
115 | } else {␊ |
116 | $tocifier = Injector::inst()->create(␊ |
117 | 'eNTiDi\Autotoc\Tocifier',␊ |
118 | $owner->getOriginalContentField()␊ |
119 | );␊ |
120 | $callback = $owner->config()->get('augment_callback');␊ |
121 | if (empty($callback)) {␊ |
122 | $callback = Config::inst()->get(self::class, 'augment_callback');␊ |
123 | }␊ |
124 | $tocifier->setAugmentCallback(explode('::', $callback));␊ |
125 | if (!$tocifier->process()) {␊ |
126 | $tocifier = false;␊ |
127 | }␊ |
128 | self::$tocifiers[$owner] = $tocifier;␊ |
129 | }␊ |
130 | ␊ |
131 | return $tocifier;␊ |
132 | }␊ |
133 | ␊ |
134 | /**␊ |
135 | * Clear the internal Autotoc cache.␊ |
136 | *␊ |
137 | * The TOC is usually cached the first time you call (directly or␊ |
138 | * indirectly) getAutotoc() or getContentField(). This method allows␊ |
139 | * to clear the internal cache to force a recomputation.␊ |
140 | */␊ |
141 | public function clearAutotoc()␊ |
142 | {␊ |
143 | unset(self::$tocifiers[$this->owner]);␊ |
144 | }␊ |
145 | ␊ |
146 | /**␊ |
147 | * Get the automatically generated table of contents.␊ |
148 | * @return ArrayData|null␊ |
149 | */␊ |
150 | public function getAutotoc()␊ |
151 | {␊ |
152 | $tocifier = self::getTocifier($this->owner);␊ |
153 | if (!$tocifier) {␊ |
154 | return null;␊ |
155 | }␊ |
156 | ␊ |
157 | $toc = $tocifier->getTOC();␊ |
158 | if (empty($toc)) {␊ |
159 | return null;␊ |
160 | }␊ |
161 | ␊ |
162 | return new ArrayData([␊ |
163 | 'Children' => self::convertChildren($toc)␊ |
164 | ]);␊ |
165 | }␊ |
166 | ␊ |
167 | /**␊ |
168 | * Get the non-augmented content field.␊ |
169 | * @return string␊ |
170 | */␊ |
171 | public function getOriginalContentField()␊ |
172 | {␊ |
173 | $model = $this->owner->getCustomisedObj();␊ |
174 | if (!$model) {␊ |
175 | $model = $this->owner->data();␊ |
176 | }␊ |
177 | if (!$model) {␊ |
178 | return null;␊ |
179 | }␊ |
180 | ␊ |
181 | $field = $this->contentField();␊ |
182 | if (!$model->hasField($field)) {␊ |
183 | return null;␊ |
184 | }␊ |
185 | ␊ |
186 | return $model->getField($field);␊ |
187 | }␊ |
188 | ␊ |
189 | /**␊ |
190 | * Get the augmented content field.␊ |
191 | * @return string␊ |
192 | */␊ |
193 | public function getContentField()␊ |
194 | {␊ |
195 | $tocifier = self::getTocifier($this->owner);␊ |
196 | if (!$tocifier) {␊ |
197 | return $this->getOriginalContentField();␊ |
198 | }␊ |
199 | ␊ |
200 | return $tocifier->getHTML();␊ |
201 | }␊ |
202 | ␊ |
203 | /**␊ |
204 | * I don't remember what the hell is this...␊ |
205 | * @return string␊ |
206 | */␊ |
207 | public function getBodyAutotoc()␊ |
208 | {␊ |
209 | return ' data-spy="scroll" data-target=".toc"';␊ |
210 | }␊ |
211 | }␊ |