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