xref: /plugin/combo/syntax/contentlist.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
19337a630SNickeau<?php
29337a630SNickeau
39337a630SNickeau
49337a630SNickeauuse ComboStrap\Call;
59337a630SNickeauuse ComboStrap\CallStack;
6*04fd306cSNickeauuse ComboStrap\DataType;
7*04fd306cSNickeauuse ComboStrap\Dimension;
8*04fd306cSNickeauuse ComboStrap\ExceptionBadArgument;
9*04fd306cSNickeauuse ComboStrap\LogUtility;
109337a630SNickeauuse ComboStrap\PluginUtility;
11*04fd306cSNickeauuse ComboStrap\Tag\BoxTag;
12*04fd306cSNickeauuse ComboStrap\TagAttribute\Align;
139337a630SNickeauuse ComboStrap\TagAttributes;
14*04fd306cSNickeauuse ComboStrap\XmlTagProcessing;
159337a630SNickeau
169337a630SNickeau
179337a630SNickeau/**
189337a630SNickeau * Class syntax_plugin_combo_list
199337a630SNickeau * Implementation of a list
209337a630SNickeau *
219337a630SNickeau * Content list is a list implementation that permits to
229337a630SNickeau * create simple and complex list such as media list
239337a630SNickeau *
249337a630SNickeau * https://getbootstrap.com/docs/4.0/layout/media-object/#media-list - Bootstrap media list
259337a630SNickeau * https://getbootstrap.com/docs/5.0/utilities/flex/#media-object
269337a630SNickeau * https://github.com/material-components/material-components-web/tree/master/packages/mdc-list - mdc list
279337a630SNickeau *
289337a630SNickeau * It's implemented on the basis of:
299337a630SNickeau *   * bootstrap list-group
309337a630SNickeau *   * flex utility on the list-group-item
319337a630SNickeau *   * with the row/cell (grid) adjusted in order to add automatically a space between col (cell)
329337a630SNickeau *
339337a630SNickeau * Note:
349337a630SNickeau *   * The cell inside a row are centered vertically automatically
359337a630SNickeau *   * The illustrative image does not get any [[ui:image#link|link]]
369337a630SNickeau *
379337a630SNickeau * Documentation:
389337a630SNickeau * https://getbootstrap.com/docs/4.1/components/list-group/
399337a630SNickeau * https://getbootstrap.com/docs/5.0/components/list-group/
409337a630SNickeau *
419337a630SNickeau * https://getbootstrap.com/docs/5.0/utilities/flex/
429337a630SNickeau * https://getbootstrap.com/docs/5.0/utilities/flex/#media-object
439337a630SNickeau *
449337a630SNickeau */
459337a630SNickeauclass syntax_plugin_combo_contentlist extends DokuWiki_Syntax_Plugin
469337a630SNickeau{
479337a630SNickeau
489337a630SNickeau    const DOKU_TAG = "contentlist";
499337a630SNickeau
509337a630SNickeau    /**
519337a630SNickeau     * To allow a minus
529337a630SNickeau     */
539337a630SNickeau    const MARKI_TAG = "content-list";
549337a630SNickeau    const COMBO_TAG_OLD = "list";
559337a630SNickeau    const COMBO_TAGS = [self::MARKI_TAG, self::COMBO_TAG_OLD];
569337a630SNickeau
579337a630SNickeau
58*04fd306cSNickeau    const FLUSH_TYPE = "flush";
59*04fd306cSNickeau    const NUMBERED = "numbered";
60*04fd306cSNickeau    const NUMBERED_DEFAULT = false;
61*04fd306cSNickeau    const CANONICAL = self::MARKI_TAG;
62*04fd306cSNickeau
63*04fd306cSNickeau    /**
64*04fd306cSNickeau     * @throws ExceptionBadArgument
65*04fd306cSNickeau     */
66*04fd306cSNickeau    private static function insertNumberedWrapperCloseTag(CallStack $callStack)
67*04fd306cSNickeau    {
68*04fd306cSNickeau
69*04fd306cSNickeau        $callStack->insertBefore(Call::createComboCall(
70*04fd306cSNickeau            BoxTag::TAG,
71*04fd306cSNickeau            DOKU_LEXER_EXIT,
72*04fd306cSNickeau            [BoxTag::HTML_TAG_ATTRIBUTE => "li"],
73*04fd306cSNickeau            null,
74*04fd306cSNickeau            null,
75*04fd306cSNickeau            null,
76*04fd306cSNickeau            null,
77*04fd306cSNickeau            \syntax_plugin_combo_xmlblocktag::TAG
78*04fd306cSNickeau        ));
79*04fd306cSNickeau
80*04fd306cSNickeau    }
81*04fd306cSNickeau
82*04fd306cSNickeau
83*04fd306cSNickeau    /**
84*04fd306cSNickeau     *
85*04fd306cSNickeau     * @param CallStack $callStack
86*04fd306cSNickeau     * @return void
87*04fd306cSNickeau     * @throws ExceptionBadArgument
88*04fd306cSNickeau     */
89*04fd306cSNickeau    private static function insertNumberedWrapperOpenTag(CallStack $callStack)
90*04fd306cSNickeau    {
91*04fd306cSNickeau        $attributesNumberedWrapper = [
92*04fd306cSNickeau            Align::ALIGN_ATTRIBUTE => Align::Y_TOP_CHILDREN, // To have the number at the top and not centered as for a combostrap flex
93*04fd306cSNickeau            TagAttributes::CLASS_KEY => syntax_plugin_combo_contentlistitem::LIST_GROUP_ITEM_CLASS,
94*04fd306cSNickeau            BoxTag::HTML_TAG_ATTRIBUTE => "li"
95*04fd306cSNickeau        ];
96*04fd306cSNickeau        $callStack->insertBefore(Call::createComboCall(
97*04fd306cSNickeau            BoxTag::TAG,
98*04fd306cSNickeau            DOKU_LEXER_ENTER,
99*04fd306cSNickeau            $attributesNumberedWrapper,
100*04fd306cSNickeau            null,
101*04fd306cSNickeau            null,
102*04fd306cSNickeau            null,
103*04fd306cSNickeau            null,
104*04fd306cSNickeau            \syntax_plugin_combo_xmlblocktag::TAG
105*04fd306cSNickeau        ));
106*04fd306cSNickeau    }
107*04fd306cSNickeau
108*04fd306cSNickeau
1099337a630SNickeau    /**
1109337a630SNickeau     * Syntax Type.
1119337a630SNickeau     *
1129337a630SNickeau     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
1139337a630SNickeau     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
1149337a630SNickeau     * @see DokuWiki_Syntax_Plugin::getType()
1159337a630SNickeau     */
116*04fd306cSNickeau    function getType(): string
1179337a630SNickeau    {
1189337a630SNickeau        return 'container';
1199337a630SNickeau    }
1209337a630SNickeau
1219337a630SNickeau    /**
1229337a630SNickeau     * How Dokuwiki will add P element
1239337a630SNickeau     *
124*04fd306cSNickeau     * * 'normal' - Inline
125*04fd306cSNickeau     *  * 'block' - Block (p are not created inside)
126*04fd306cSNickeau     *  * 'stack' - Block (p can be created inside)
1279337a630SNickeau     *
1289337a630SNickeau     * @see DokuWiki_Syntax_Plugin::getPType()
1299337a630SNickeau     * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype
1309337a630SNickeau     */
131*04fd306cSNickeau    function getPType(): string
1329337a630SNickeau    {
133*04fd306cSNickeau        return 'stack';
1349337a630SNickeau    }
1359337a630SNickeau
1369337a630SNickeau    /**
1379337a630SNickeau     * @return array
1389337a630SNickeau     * Allow which kind of plugin inside
1399337a630SNickeau     *
1409337a630SNickeau     * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
1419337a630SNickeau     * because we manage self the content and we call self the parser
1429337a630SNickeau     *
1439337a630SNickeau     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
1449337a630SNickeau     */
145*04fd306cSNickeau    function getAllowedTypes(): array
1469337a630SNickeau    {
1479337a630SNickeau        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
1489337a630SNickeau    }
1499337a630SNickeau
150*04fd306cSNickeau    public function accepts($mode): bool
1519337a630SNickeau    {
1529337a630SNickeau
1539337a630SNickeau        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
1549337a630SNickeau
1559337a630SNickeau    }
1569337a630SNickeau
1579337a630SNickeau
158*04fd306cSNickeau    function getSort(): int
1599337a630SNickeau    {
1609337a630SNickeau        return 15;
1619337a630SNickeau    }
1629337a630SNickeau
1639337a630SNickeau
1649337a630SNickeau    function connectTo($mode)
1659337a630SNickeau    {
1669337a630SNickeau
1679337a630SNickeau        foreach (self::COMBO_TAGS as $tag) {
168*04fd306cSNickeau            $pattern = XmlTagProcessing::getContainerTagPattern($tag);
1699337a630SNickeau            $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
1709337a630SNickeau        }
1719337a630SNickeau
1729337a630SNickeau    }
1739337a630SNickeau
1749337a630SNickeau    public function postConnect()
1759337a630SNickeau    {
1769337a630SNickeau        foreach (self::COMBO_TAGS as $tag) {
1779337a630SNickeau            $this->Lexer->addExitPattern('</' . $tag . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
1789337a630SNickeau        }
1799337a630SNickeau
1809337a630SNickeau    }
1819337a630SNickeau
1829337a630SNickeau
1839337a630SNickeau    /**
1849337a630SNickeau     *
1859337a630SNickeau     * The handle function goal is to parse the matched syntax through the pattern function
1869337a630SNickeau     * and to return the result for use in the renderer
1879337a630SNickeau     * This result is always cached until the page is modified.
1889337a630SNickeau     * @param string $match
1899337a630SNickeau     * @param int $state
1909337a630SNickeau     * @param int $pos - byte position in the original source file
1919337a630SNickeau     * @param Doku_Handler $handler
192*04fd306cSNickeau     * @return array
1939337a630SNickeau     * @see DokuWiki_Syntax_Plugin::handle()
1949337a630SNickeau     *
1959337a630SNickeau     */
196*04fd306cSNickeau    function handle($match, $state, $pos, Doku_Handler $handler): array
1979337a630SNickeau    {
1989337a630SNickeau
1999337a630SNickeau        switch ($state) {
2009337a630SNickeau
2019337a630SNickeau            case DOKU_LEXER_ENTER :
2029337a630SNickeau
203*04fd306cSNickeau                $knownType = [self::FLUSH_TYPE];
204*04fd306cSNickeau                $default = [
205*04fd306cSNickeau                    Dimension::WIDTH_KEY => "fit",
206*04fd306cSNickeau                    self::NUMBERED => self::NUMBERED_DEFAULT
207*04fd306cSNickeau                ];
208*04fd306cSNickeau                $attributes = TagAttributes::createFromTagMatch($match, $default, $knownType);
2099337a630SNickeau
2109337a630SNickeau                if ($attributes->hasComponentAttribute(TagAttributes::TYPE_KEY)) {
2119337a630SNickeau                    $type = trim(strtolower($attributes->getType()));
212*04fd306cSNickeau                    if ($type === self::FLUSH_TYPE) {
2139337a630SNickeau                        // https://getbootstrap.com/docs/5.0/components/list-group/#flush
2149337a630SNickeau                        // https://getbootstrap.com/docs/4.1/components/list-group/#flush
2159337a630SNickeau                        $attributes->addClassName("list-group-flush");
2169337a630SNickeau                    }
2179337a630SNickeau                }
218*04fd306cSNickeau
2199337a630SNickeau                return array(
2209337a630SNickeau                    PluginUtility::STATE => $state,
2219337a630SNickeau                    PluginUtility::ATTRIBUTES => $attributes->toCallStackArray()
2229337a630SNickeau                );
2239337a630SNickeau
2249337a630SNickeau            case DOKU_LEXER_UNMATCHED :
2259337a630SNickeau
2269337a630SNickeau                return PluginUtility::handleAndReturnUnmatchedData(self::MARKI_TAG, $match, $handler);
2279337a630SNickeau
2289337a630SNickeau            case DOKU_LEXER_EXIT :
2299337a630SNickeau
2309337a630SNickeau                /**
231*04fd306cSNickeau                 * Add to all children the list-group-item
2329337a630SNickeau                 */
2339337a630SNickeau                $callStack = CallStack::createFromHandler($handler);
234*04fd306cSNickeau                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
2359337a630SNickeau
236*04fd306cSNickeau                /**
237*04fd306cSNickeau                 * The number are inside (this is a **content** list)
238*04fd306cSNickeau                 * and not as with a marker box, outside.
239*04fd306cSNickeau                 *
240*04fd306cSNickeau                 * It's in the `before` box and is therefore a invisible box
241*04fd306cSNickeau                 * To make it easy for the user (it does need to known that),
242*04fd306cSNickeau                 * we wrap the user markup in a flex with a top placement
243*04fd306cSNickeau                 */
244*04fd306cSNickeau                $numbered = DataType::toBoolean($openingTag->getAttribute(self::NUMBERED, self::NUMBERED_DEFAULT));
245*04fd306cSNickeau                if ($numbered === true) {
246*04fd306cSNickeau                    $firstChild = $callStack->moveToFirstChildTag();
247*04fd306cSNickeau                    if ($firstChild !== false) {
248*04fd306cSNickeau                        try {
249*04fd306cSNickeau                            self::insertNumberedWrapperOpenTag($callStack);
250*04fd306cSNickeau                            while ($callStack->moveToNextSiblingTag()) {
251*04fd306cSNickeau                                self::insertNumberedWrapperCloseTag($callStack);
252*04fd306cSNickeau                                self::insertNumberedWrapperOpenTag($callStack);
253*04fd306cSNickeau                            }
254*04fd306cSNickeau                            self::insertNumberedWrapperCloseTag($callStack);
255*04fd306cSNickeau                        } catch (ExceptionBadArgument $e) {
256*04fd306cSNickeau                            LogUtility::error("We were unable to wrap the content list to enable numbering placement. Error: {$e->getMessage()}", self::CANONICAL);
257*04fd306cSNickeau                        }
258*04fd306cSNickeau                    }
259*04fd306cSNickeau                } else {
260*04fd306cSNickeau                    foreach ($callStack->getChildren() as $child) {
261*04fd306cSNickeau                        $child->addClassName(syntax_plugin_combo_contentlistitem::LIST_GROUP_ITEM_CLASS);
262*04fd306cSNickeau                        if ($child->getTagName() === BoxTag::HTML_TAG_ATTRIBUTE) {
263*04fd306cSNickeau                            $child->addAttribute(BoxTag::HTML_TAG_ATTRIBUTE, "li");
264*04fd306cSNickeau                        }
2659337a630SNickeau                    }
2669337a630SNickeau                }
2679337a630SNickeau
268*04fd306cSNickeau                return array(
269*04fd306cSNickeau                    PluginUtility::STATE => $state,
270*04fd306cSNickeau                    PluginUtility::ATTRIBUTES => $openingTag->getAttributes()
271*04fd306cSNickeau                );
2729337a630SNickeau
2739337a630SNickeau
2749337a630SNickeau        }
2759337a630SNickeau        return array();
2769337a630SNickeau
2779337a630SNickeau    }
2789337a630SNickeau
2799337a630SNickeau    /**
2809337a630SNickeau     * Render the output
2819337a630SNickeau     * @param string $format
2829337a630SNickeau     * @param Doku_Renderer $renderer
2839337a630SNickeau     * @param array $data - what the function handle() return'ed
2849337a630SNickeau     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
2859337a630SNickeau     * @see DokuWiki_Syntax_Plugin::render()
2869337a630SNickeau     *
2879337a630SNickeau     *
2889337a630SNickeau     */
289*04fd306cSNickeau    function render($format, Doku_Renderer $renderer, $data): bool
2909337a630SNickeau    {
2919337a630SNickeau        if ($format == 'xhtml') {
2929337a630SNickeau
2939337a630SNickeau            /** @var Doku_Renderer_xhtml $renderer */
2949337a630SNickeau            $state = $data[PluginUtility::STATE];
2959337a630SNickeau            switch ($state) {
2969337a630SNickeau                case DOKU_LEXER_ENTER :
2979337a630SNickeau
298*04fd306cSNickeau                    PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(self::MARKI_TAG);
2999337a630SNickeau                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES], self::MARKI_TAG);
3009337a630SNickeau                    $tagAttributes->addClassName("list-group");
3019337a630SNickeau
302*04fd306cSNickeau                    $numbered = $tagAttributes->getBooleanValueAndRemoveIfPresent(self::NUMBERED, self::NUMBERED_DEFAULT);
303*04fd306cSNickeau
304*04fd306cSNickeau                    $htmlElement = "ul";
305*04fd306cSNickeau                    if ($numbered) {
306*04fd306cSNickeau                        $tagAttributes->addClassName("list-group-numbered");
307*04fd306cSNickeau                        $htmlElement = "ol";
308*04fd306cSNickeau                    }
309*04fd306cSNickeau
310*04fd306cSNickeau                    $renderer->doc .= $tagAttributes->toHtmlEnterTag($htmlElement);
3119337a630SNickeau                    break;
3129337a630SNickeau                case DOKU_LEXER_UNMATCHED :
313*04fd306cSNickeau
3149337a630SNickeau                    $renderer->doc .= PluginUtility::renderUnmatched($data);
3159337a630SNickeau                    break;
316*04fd306cSNickeau
317*04fd306cSNickeau                case DOKU_LEXER_EXIT :
318*04fd306cSNickeau                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES], self::MARKI_TAG);
319*04fd306cSNickeau                    $numbered = $tagAttributes->getValueAndRemoveIfPresent(self::NUMBERED, self::NUMBERED_DEFAULT);
320*04fd306cSNickeau                    $htmlElement = "ul";
321*04fd306cSNickeau                    if ($numbered) {
322*04fd306cSNickeau                        $htmlElement = "ol";
323*04fd306cSNickeau                    }
324*04fd306cSNickeau                    $renderer->doc .= "</$htmlElement>";
325*04fd306cSNickeau                    break;
3269337a630SNickeau            }
3279337a630SNickeau            return true;
3289337a630SNickeau        }
3299337a630SNickeau
3309337a630SNickeau        // unsupported $mode
3319337a630SNickeau        return false;
3329337a630SNickeau    }
3339337a630SNickeau
3349337a630SNickeau
3359337a630SNickeau}
3369337a630SNickeau
337