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