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