1<?php 2 3 4use ComboStrap\Call; 5use ComboStrap\CallStack; 6use ComboStrap\DataType; 7use ComboStrap\Dimension; 8use ComboStrap\ExceptionBadArgument; 9use ComboStrap\LogUtility; 10use ComboStrap\PluginUtility; 11use ComboStrap\Tag\BoxTag; 12use ComboStrap\TagAttribute\Align; 13use ComboStrap\TagAttributes; 14use ComboStrap\XmlTagProcessing; 15 16 17/** 18 * Class syntax_plugin_combo_list 19 * Implementation of a list 20 * 21 * Content list is a list implementation that permits to 22 * create simple and complex list such as media list 23 * 24 * https://getbootstrap.com/docs/4.0/layout/media-object/#media-list - Bootstrap media list 25 * https://getbootstrap.com/docs/5.0/utilities/flex/#media-object 26 * https://github.com/material-components/material-components-web/tree/master/packages/mdc-list - mdc list 27 * 28 * It's implemented on the basis of: 29 * * bootstrap list-group 30 * * flex utility on the list-group-item 31 * * with the row/cell (grid) adjusted in order to add automatically a space between col (cell) 32 * 33 * Note: 34 * * The cell inside a row are centered vertically automatically 35 * * The illustrative image does not get any [[ui:image#link|link]] 36 * 37 * Documentation: 38 * https://getbootstrap.com/docs/4.1/components/list-group/ 39 * https://getbootstrap.com/docs/5.0/components/list-group/ 40 * 41 * https://getbootstrap.com/docs/5.0/utilities/flex/ 42 * https://getbootstrap.com/docs/5.0/utilities/flex/#media-object 43 * 44 */ 45class syntax_plugin_combo_contentlist extends DokuWiki_Syntax_Plugin 46{ 47 48 const DOKU_TAG = "contentlist"; 49 50 /** 51 * To allow a minus 52 */ 53 const MARKI_TAG = "content-list"; 54 const COMBO_TAG_OLD = "list"; 55 const COMBO_TAGS = [self::MARKI_TAG, self::COMBO_TAG_OLD]; 56 57 58 const FLUSH_TYPE = "flush"; 59 const NUMBERED = "numbered"; 60 const NUMBERED_DEFAULT = false; 61 const CANONICAL = self::MARKI_TAG; 62 63 /** 64 * @throws ExceptionBadArgument 65 */ 66 private static function insertNumberedWrapperCloseTag(CallStack $callStack) 67 { 68 69 $callStack->insertBefore(Call::createComboCall( 70 BoxTag::TAG, 71 DOKU_LEXER_EXIT, 72 [BoxTag::HTML_TAG_ATTRIBUTE => "li"], 73 null, 74 null, 75 null, 76 null, 77 \syntax_plugin_combo_xmlblocktag::TAG 78 )); 79 80 } 81 82 83 /** 84 * 85 * @param CallStack $callStack 86 * @return void 87 * @throws ExceptionBadArgument 88 */ 89 private static function insertNumberedWrapperOpenTag(CallStack $callStack) 90 { 91 $attributesNumberedWrapper = [ 92 Align::ALIGN_ATTRIBUTE => Align::Y_TOP_CHILDREN, // To have the number at the top and not centered as for a combostrap flex 93 TagAttributes::CLASS_KEY => syntax_plugin_combo_contentlistitem::LIST_GROUP_ITEM_CLASS, 94 BoxTag::HTML_TAG_ATTRIBUTE => "li" 95 ]; 96 $callStack->insertBefore(Call::createComboCall( 97 BoxTag::TAG, 98 DOKU_LEXER_ENTER, 99 $attributesNumberedWrapper, 100 null, 101 null, 102 null, 103 null, 104 \syntax_plugin_combo_xmlblocktag::TAG 105 )); 106 } 107 108 109 /** 110 * Syntax Type. 111 * 112 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 113 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 114 * @see DokuWiki_Syntax_Plugin::getType() 115 */ 116 function getType(): string 117 { 118 return 'container'; 119 } 120 121 /** 122 * How Dokuwiki will add P element 123 * 124 * * 'normal' - Inline 125 * * 'block' - Block (p are not created inside) 126 * * 'stack' - Block (p can be created inside) 127 * 128 * @see DokuWiki_Syntax_Plugin::getPType() 129 * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype 130 */ 131 function getPType(): string 132 { 133 return 'stack'; 134 } 135 136 /** 137 * @return array 138 * Allow which kind of plugin inside 139 * 140 * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 141 * because we manage self the content and we call self the parser 142 * 143 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 144 */ 145 function getAllowedTypes(): array 146 { 147 return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 148 } 149 150 public function accepts($mode): bool 151 { 152 153 return syntax_plugin_combo_preformatted::disablePreformatted($mode); 154 155 } 156 157 158 function getSort(): int 159 { 160 return 15; 161 } 162 163 164 function connectTo($mode) 165 { 166 167 foreach (self::COMBO_TAGS as $tag) { 168 $pattern = XmlTagProcessing::getContainerTagPattern($tag); 169 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 170 } 171 172 } 173 174 public function postConnect() 175 { 176 foreach (self::COMBO_TAGS as $tag) { 177 $this->Lexer->addExitPattern('</' . $tag . '>', PluginUtility::getModeFromTag($this->getPluginComponent())); 178 } 179 180 } 181 182 183 /** 184 * 185 * The handle function goal is to parse the matched syntax through the pattern function 186 * and to return the result for use in the renderer 187 * This result is always cached until the page is modified. 188 * @param string $match 189 * @param int $state 190 * @param int $pos - byte position in the original source file 191 * @param Doku_Handler $handler 192 * @return array 193 * @see DokuWiki_Syntax_Plugin::handle() 194 * 195 */ 196 function handle($match, $state, $pos, Doku_Handler $handler): array 197 { 198 199 switch ($state) { 200 201 case DOKU_LEXER_ENTER : 202 203 $knownType = [self::FLUSH_TYPE]; 204 $default = [ 205 Dimension::WIDTH_KEY => "fit", 206 self::NUMBERED => self::NUMBERED_DEFAULT 207 ]; 208 $attributes = TagAttributes::createFromTagMatch($match, $default, $knownType); 209 210 if ($attributes->hasComponentAttribute(TagAttributes::TYPE_KEY)) { 211 $type = trim(strtolower($attributes->getType())); 212 if ($type === self::FLUSH_TYPE) { 213 // https://getbootstrap.com/docs/5.0/components/list-group/#flush 214 // https://getbootstrap.com/docs/4.1/components/list-group/#flush 215 $attributes->addClassName("list-group-flush"); 216 } 217 } 218 219 return array( 220 PluginUtility::STATE => $state, 221 PluginUtility::ATTRIBUTES => $attributes->toCallStackArray() 222 ); 223 224 case DOKU_LEXER_UNMATCHED : 225 226 return PluginUtility::handleAndReturnUnmatchedData(self::MARKI_TAG, $match, $handler); 227 228 case DOKU_LEXER_EXIT : 229 230 /** 231 * Add to all children the list-group-item 232 */ 233 $callStack = CallStack::createFromHandler($handler); 234 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 235 236 /** 237 * The number are inside (this is a **content** list) 238 * and not as with a marker box, outside. 239 * 240 * It's in the `before` box and is therefore a invisible box 241 * To make it easy for the user (it does need to known that), 242 * we wrap the user markup in a flex with a top placement 243 */ 244 $numbered = DataType::toBoolean($openingTag->getAttribute(self::NUMBERED, self::NUMBERED_DEFAULT)); 245 if ($numbered === true) { 246 $firstChild = $callStack->moveToFirstChildTag(); 247 if ($firstChild !== false) { 248 try { 249 self::insertNumberedWrapperOpenTag($callStack); 250 while ($callStack->moveToNextSiblingTag()) { 251 self::insertNumberedWrapperCloseTag($callStack); 252 self::insertNumberedWrapperOpenTag($callStack); 253 } 254 self::insertNumberedWrapperCloseTag($callStack); 255 } catch (ExceptionBadArgument $e) { 256 LogUtility::error("We were unable to wrap the content list to enable numbering placement. Error: {$e->getMessage()}", self::CANONICAL); 257 } 258 } 259 } else { 260 foreach ($callStack->getChildren() as $child) { 261 $child->addClassName(syntax_plugin_combo_contentlistitem::LIST_GROUP_ITEM_CLASS); 262 if ($child->getTagName() === BoxTag::HTML_TAG_ATTRIBUTE) { 263 $child->addAttribute(BoxTag::HTML_TAG_ATTRIBUTE, "li"); 264 } 265 } 266 } 267 268 return array( 269 PluginUtility::STATE => $state, 270 PluginUtility::ATTRIBUTES => $openingTag->getAttributes() 271 ); 272 273 274 } 275 return array(); 276 277 } 278 279 /** 280 * Render the output 281 * @param string $format 282 * @param Doku_Renderer $renderer 283 * @param array $data - what the function handle() return'ed 284 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 285 * @see DokuWiki_Syntax_Plugin::render() 286 * 287 * 288 */ 289 function render($format, Doku_Renderer $renderer, $data): bool 290 { 291 if ($format == 'xhtml') { 292 293 /** @var Doku_Renderer_xhtml $renderer */ 294 $state = $data[PluginUtility::STATE]; 295 switch ($state) { 296 case DOKU_LEXER_ENTER : 297 298 PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(self::MARKI_TAG); 299 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES], self::MARKI_TAG); 300 $tagAttributes->addClassName("list-group"); 301 302 $numbered = $tagAttributes->getBooleanValueAndRemoveIfPresent(self::NUMBERED, self::NUMBERED_DEFAULT); 303 304 $htmlElement = "ul"; 305 if ($numbered) { 306 $tagAttributes->addClassName("list-group-numbered"); 307 $htmlElement = "ol"; 308 } 309 310 $renderer->doc .= $tagAttributes->toHtmlEnterTag($htmlElement); 311 break; 312 case DOKU_LEXER_UNMATCHED : 313 314 $renderer->doc .= PluginUtility::renderUnmatched($data); 315 break; 316 317 case DOKU_LEXER_EXIT : 318 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES], self::MARKI_TAG); 319 $numbered = $tagAttributes->getValueAndRemoveIfPresent(self::NUMBERED, self::NUMBERED_DEFAULT); 320 $htmlElement = "ul"; 321 if ($numbered) { 322 $htmlElement = "ol"; 323 } 324 $renderer->doc .= "</$htmlElement>"; 325 break; 326 } 327 return true; 328 } 329 330 // unsupported $mode 331 return false; 332 } 333 334 335} 336 337