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