isDescendantOf(self::TABLE_TAGS); } public static function isBlockTag(string $tagName): bool { return \preg_match('~^(?:' . self::BLOCK_TAGS . ')$~i', $tagName) === 1; } /** * Whether the given output would start an HTML block when at the start of a line */ public static function startsBlock(string $markdown): bool { return \preg_match('~^[ \t]*+<(?:[!?]|/?(?:' . self::BLOCK_TAGS . ')(?:[\s/>]|$))~i', $markdown) !== 0; } /** * Whether a fenced code block for the element wouldn't reliably be recognized as one */ public static function cannotBeFenced(ElementInterface $element, ?Configuration $config): bool { if ($config === null) { return self::isInsideInlineMarkdown($element, false); } if ($config->getOption(self::KEEPS_ALL_CODE_AS_HTML) === true) { return true; } $isInsideInlineMarkdown = $config->getOption(self::IS_INSIDE_INLINE_MARKDOWN); return \is_bool($isInsideInlineMarkdown) ? $isInsideInlineMarkdown : self::isInsideInlineMarkdown($element, $config->getOption(self::FLATTENS_TABLE_CELLS) === true); } /** * Whether the element is inside of something which puts its contents on the line it starts on, * where nothing which has to start a line of its own would be recognized * * @param bool $flattensTableCells Whether table cells are converted to Markdown, which puts each row on one line */ public static function isInsideInlineMarkdown(ElementInterface $element, bool $flattensTableCells): bool { for ($node = $element->getParent(); $node !== null; $node = $node->getParent()) { if (self::putsContentsInline($node, $flattensTableCells)) { return true; } } return false; } /** * Whether the element puts its own contents on the line it starts on, regardless of what's around it */ public static function putsContentsInline(ElementInterface $node, bool $flattensTableCells): bool { $parent = $node->getParent(); $tag = $node->getTagName(); if (\in_array($tag, ['a', 'b', 'em', 'i', 'strong', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'], true)) { return true; } // As HTML, these make up a block which a blank line doesn't end if (\in_array($tag, ['script', 'style', 'textarea'], true)) { return true; } if ($flattensTableCells && \in_array($tag, self::TABLE_TAGS, true)) { return true; } // A list item anywhere else isn't output as part of a list which starts on its own line if ($tag === 'li' && ($parent === null || ! \in_array($parent->getTagName(), ['ol', 'ul'], true))) { return true; } // Nor is one which follows something other than a list item if ($tag === 'li' && ! self::startsLine($node)) { return true; } // A list at the very start of a list item shares its line, which leaves the rest of it misaligned return ($tag === 'ol' || $tag === 'ul') && self::startsListItem($node); } /** * Whether a list item comes first in its list, or after something which ended its line, as a list item does */ public static function startsLine(ElementInterface $listItem): bool { $end = PrecedingMarkdown::getEndOfSiblings($listItem); return \trim($end, " \t") === '' || \substr($end, -1) === "\n"; } private static function startsListItem(ElementInterface $element): bool { for ($node = $element; ($parent = $node->getParent()) !== null; $node = $parent) { if (PrecedingMarkdown::hasContent($node)) { return false; } if ($parent->getTagName() === 'li') { return true; } } return false; } /** * Keeps the element as HTML around the Markdown which its children were converted to */ public static function fromElement(ElementInterface $element): string { $html = $element->getChildrenAsString(); // The attributes must stay encoded, otherwise their values could break out of their quotes if (\preg_match('/^<([^\s>\/!?=]++)(?:\s++[^\s=>]++="[^"]*+")*+\s*+>/', $html, $matches) === 1) { return Backticks::escapeText($matches[0]) . $element->getValue() . ''; } // Nodes which aren't elements have no tag to keep if (\preg_match('/^<[^!?]/', $html) !== 1) { return \html_entity_decode($html); } $tag = $element->getTagName(); return '<' . $tag . '>' . $element->getValue() . ''; } public static function getCode(ElementInterface $element): string { // Inside a
, the code ends up as part of its content, which leaves out any nested 
 tags as well
        $code = self::getContentWithoutTags($element, $element->isDescendantOf('pre') ? ['code', 'pre'] : ['code']);

        // A carriage return alone also ends a line, and would be left out of any indentation added later
        $code = \preg_replace('/\r\n?/', "\n", $code);
        \assert($code !== null);

        return $code;
    }

    /**
     * Returns the element's contents as text, without the tags of any elements with the given names, its own included
     *
     * @param string[] $tagNames
     */
    public static function getContentWithoutTags(ElementInterface $element, array $tagNames): string
    {
        // Text is still escaped here, so only actual tags match, whose attributes are always written as name="value"
        $html = \preg_replace(
            '~]++="[^"]*+")*+\s*+>~',
            '',
            $element->getChildrenAsString()
        );
        \assert($html !== null);

        return \html_entity_decode($html);
    }

    /**
     * Returns an element holding the code which is safe both as raw HTML and as inline HTML surrounded by Markdown
     */
    public static function fromCode(string $code, string $tag = 'code'): string
    {
        $code = \preg_replace_callback('/[!-\/:-@\[-`{-~]/', static function (array $matches): string {
            return '&#' . \ord($matches[0]) . ';';
        }, $code);
        \assert($code !== null);

        // An actual line break could end the HTML block which this is in
        $code = \preg_replace('/\r\n|\r|\n/', $tag === 'pre' ? '
' : '', $code);
        \assert($code !== null);

        return $code === '' ? '' : '<' . $tag . '>' . $code . '';
    }
}