1<?php 2 3namespace ComboStrap; 4 5 6class OutlineSection extends TreeNode 7{ 8 const CANONICAL = "outline"; 9 10 11 /** 12 * Not to confound with header calls that are {@link OutlineSection::getContentCalls()} 13 * of a section that has children 14 * 15 * @var Call[] $headingCalls 16 */ 17 private array $headingCalls = []; 18 /** 19 * 20 * @var Call[] $contentCalls 21 */ 22 private array $contentCalls = []; 23 24 25 private string $headingId; 26 27 private int $startFileIndex; 28 private ?int $endFileIndex = null; 29 30 /** 31 * @var Call|null - the first heading call for the section 32 */ 33 private ?Call $headingEnterCall; 34 /** 35 * @var array an array to make sure that the id are unique 36 */ 37 private array $tocUniqueId = []; 38 39 /** 40 * @var int - a best guess on the number of 41 */ 42 private int $lineNumber; 43 44 45 /** 46 * @param Call|null $headingEnterCall - null if the section is the root 47 */ 48 private function __construct(Call $headingEnterCall = null) 49 { 50 $this->headingEnterCall = $headingEnterCall; 51 if ($headingEnterCall !== null) { 52 $position = $headingEnterCall->getFirstMatchedCharacterPosition(); 53 if ($position === null) { 54 $this->startFileIndex = 0; 55 } else { 56 $this->startFileIndex = $position; 57 } 58 $this->addHeaderCall($headingEnterCall); 59 } else { 60 $this->startFileIndex = 0; 61 } 62 $this->lineNumber = 1; // the heading 63 64 } 65 66 67 public static function createOutlineRoot(): OutlineSection 68 { 69 return new OutlineSection(null); 70 } 71 72 73 /** 74 * Return a text to an HTML Id 75 * @param string $fragment 76 * @return string 77 */ 78 public static function textToHtmlSectionId(string $fragment): string 79 { 80 $check = false; 81 // for empty string, the below function returns `section` 82 return sectionID($fragment, $check); 83 } 84 85 public static function createFromEnterHeadingCall(Call $enterHeadingCall): OutlineSection 86 { 87 return new OutlineSection($enterHeadingCall); 88 } 89 90 public function getFirstChild(): OutlineSection 91 { 92 93 /** @noinspection PhpIncompatibleReturnTypeInspection */ 94 return parent::getFirstChild(); 95 96 } 97 98 99 public function addContentCall(Call $actualCall): OutlineSection 100 { 101 102 $this->contentCalls[] = $actualCall; 103 return $this; 104 105 106 } 107 108 public function addHeaderCall(Call $actualCall): OutlineSection 109 { 110 111 $this->headingCalls[] = $actualCall; 112 return $this; 113 } 114 115 public function getLabel(): string 116 { 117 $label = ""; 118 foreach ($this->headingCalls as $call) { 119 if ($call->getTagName() === Outline::DOKUWIKI_HEADING_CALL_NAME) { 120 $label = $call->getInstructionCall()[1][0]; 121 // no more label call 122 break; 123 } 124 if ($call->isTextCall()) { 125 // Building the text for the toc 126 // only cdata for now 127 // no image, ... 128 if ($label != "") { 129 $label .= " "; 130 } 131 $label .= trim($call->getCapturedContent()); 132 } 133 } 134 return trim($label); 135 } 136 137 public function setStartPosition(int $startPosition): OutlineSection 138 { 139 $this->startFileIndex = $startPosition; 140 return $this; 141 } 142 143 public function setEndPosition(int $endFileIndex): OutlineSection 144 { 145 $this->endFileIndex = $endFileIndex; 146 return $this; 147 } 148 149 /** 150 * @return Call[] 151 */ 152 public function getHeadingCalls(): array 153 { 154 if ( 155 $this->headingEnterCall !== null && 156 $this->headingEnterCall->isPluginCall() && 157 !$this->headingEnterCall->hasAttribute("id") 158 ) { 159 $this->headingEnterCall->addAttribute("id", $this->getHeadingId()); 160 } 161 return $this->headingCalls; 162 } 163 164 165 public 166 function getEnterHeadingCall(): ?Call 167 { 168 return $this->headingEnterCall; 169 } 170 171 172 public 173 function getCalls(): array 174 { 175 return array_merge($this->headingCalls, $this->contentCalls); 176 } 177 178 public 179 function getContentCalls(): array 180 { 181 return $this->contentCalls; 182 } 183 184 /** 185 * @return int 186 */ 187 public 188 function getLevel(): int 189 { 190 if ($this->headingEnterCall === null) { 191 return 0; 192 } 193 switch ($this->headingEnterCall->getTagName()) { 194 case Outline::DOKUWIKI_HEADING_CALL_NAME: 195 $level = $this->headingEnterCall->getInstructionCall()[1][1]; 196 break; 197 default: 198 $level = $this->headingEnterCall->getAttribute(HeadingTag::LEVEL); 199 break; 200 } 201 202 try { 203 return DataType::toInteger($level); 204 } catch (ExceptionBadArgument $e) { 205 // should not happen 206 LogUtility::internalError("The level ($level) could not be cast to an integer", self::CANONICAL); 207 return 0; 208 } 209 } 210 211 public 212 function getStartPosition(): int 213 { 214 return $this->startFileIndex; 215 } 216 217 public 218 function getEndPosition(): ?int 219 { 220 return $this->endFileIndex; 221 } 222 223 public 224 function hasContentCall(): bool 225 { 226 return sizeof($this->contentCalls) > 0; 227 } 228 229 /** 230 */ 231 public 232 function getHeadingId() 233 { 234 235 if (!isset($this->headingId)) { 236 $id = $this->headingEnterCall->getAttribute("id"); 237 if ($id !== null) { 238 return $id; 239 } 240 $label = $this->getLabel(); 241 $this->headingId = sectionID($label, $this->tocUniqueId); 242 } 243 return $this->headingId; 244 245 } 246 247 /** 248 * A HTML section should have a heading 249 * but in a markup document, we may have data before the first 250 * heading making a section without heading 251 * @return bool 252 */ 253 public 254 function hasHeading(): bool 255 { 256 return $this->headingEnterCall !== null; 257 } 258 259 /** 260 * @return OutlineSection[] 261 */ 262 public 263 function getChildren(): array 264 { 265 return parent::getChildren(); 266 } 267 268 public function setLevel(int $level): OutlineSection 269 { 270 switch ($this->headingEnterCall->getTagName()) { 271 case Outline::DOKUWIKI_HEADING_CALL_NAME: 272 $this->headingEnterCall->getInstructionCall()[1][1] = $level; 273 break; 274 default: 275 $this->headingEnterCall->setAttribute(HeadingTag::LEVEL, $level); 276 $headingExitCall = $this->headingCalls[count($this->headingCalls) - 1]; 277 $headingExitCall->setAttribute(HeadingTag::LEVEL, $level); 278 break; 279 } 280 281 /** 282 * Update the descdenants sections 283 * @param OutlineSection $parentSection 284 * @return void 285 */ 286 $updateLevel = function (OutlineSection $parentSection) { 287 foreach ($parentSection->getChildren() as $child) { 288 $child->setLevel($parentSection->getLevel() + 1); 289 } 290 }; 291 TreeVisit::visit($this, $updateLevel); 292 293 return $this; 294 } 295 296 297 public function deleteContentCalls(): OutlineSection 298 { 299 $this->contentCalls = []; 300 return $this; 301 } 302 303 public function incrementLineNumber(): OutlineSection 304 { 305 $this->lineNumber++; 306 return $this; 307 } 308 309 public function getLineCount(): int 310 { 311 return $this->lineNumber; 312 } 313 314 315} 316