src/render/HTMLRenderer.php (311 lines of code) (raw):

<?hh // strict /* * Copyright (c) 2004-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ namespace Facebook\Markdown; use namespace HH\Lib\{C, Str, Vec}; // TODO: fix namespace support in XHP, use that :'( class HTMLRenderer extends Renderer<string> { const keyset<classname<RenderFilter>> EXTENSIONS = keyset[ TagFilterExtension::class, ]; protected static function escapeContent(string $text): string { return _Private\plain_text_to_html($text); } protected static function escapeAttribute(string $text): string { return _Private\plain_text_to_html_attribute($text); } // This is the list from the reference implementation //hackfmt-ignore const keyset<string> URI_SAFE = keyset[ '-', '_', '.', '+', '!', '*', "'", '(', ')', ';', ':', '%', '#', '@', '?', '=', ';', ':', '/', ',', '+', '&', '$', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ]; protected static function escapeURIAttribute(string $text): string { // While the spec states that no particular method is required, we attempt // to match cmark's behavior so that we can run the spec test suite. $text = \html_entity_decode( $text, /* HH_FIXME[4106] */ /* HH_FIXME[2049] */ \ENT_HTML5, 'UTF-8', ); $out = ''; $len = Str\length($text); for ($i = 0; $i < $len; ++$i) { $char = $text[$i]; if (C\contains_key(self::URI_SAFE, $char)) { $out .= $char; continue; } $out .= \urlencode($char); } $text = $out; return self::escapeAttribute($text); } <<__Override>> protected function renderNodes(vec<ASTNode> $nodes): string { return $nodes |> Vec\map($$, $node ==> $this->render($node)) |> Vec\filter($$, $line ==> $line !== '') |> Str\join($$, ''); } <<__Override>> protected function renderResolvedNode(ASTNode $node): string { if ($node is RenderableAsHTML) { return $node->renderAsHTML($this->getContext(), $this); } return parent::renderResolvedNode($node); } <<__Override>> protected function renderBlankLine(): string { return ''; } <<__Override>> protected function renderBlockQuote(Blocks\BlockQuote $node): string { return $node->getChildren() |> $this->renderNodes($$) |> "<blockquote>\n".$$."</blockquote>\n"; } <<__Override>> protected function renderCodeBlock(Blocks\CodeBlock $node): string { $extra = ''; $info = $node->getInfoString(); if ($info !== null) { $first = C\firstx(Str\split($info, ' ')); $extra = ' class="language-'.self::escapeAttribute($first).'"'; } $code = $node->getCode(); if ($code !== '') { $code = self::escapeContent($code)."\n"; } return '<pre><code'.$extra.'>'.$code."</code></pre>\n"; } <<__Override>> protected function renderHeading(Blocks\Heading $node): string { $level = $node->getLevel(); return $node->getHeading() |> $this->renderNodes($$) |> \sprintf("<h%d>%s</h%d>\n", $level, $$, $level); } <<__Override>> protected function renderHTMLBlock(Blocks\HTMLBlock $node): string { return $node->getCode()."\n"; } <<__Override>> protected function renderLinkReferenceDefinition( Blocks\LinkReferenceDefinition $_def, ): string { return ''; } protected function renderTaskListItemExtension( Blocks\ListOfItems $list, Blocks\TaskListItemExtension $item, ): string { $checked = $item->isChecked() ? ' checked=""' : ''; $checkbox = '<input'.$checked.' disabled="" type="checkbox"> '; $children = $item->getChildren(); $first = C\first($children); if ($first is Blocks\Paragraph) { $children[0] = new Blocks\Paragraph( Vec\concat(vec[new Inlines\RawHTML($checkbox)], $first->getContents()), ); } else { $children = Vec\concat(vec[new Blocks\HTMLBlock($checkbox)], $children); } return $this->renderListItem( $list, new Blocks\ListItem($item->getNumber(), $children), ); } protected function renderListItem( Blocks\ListOfItems $list, Blocks\ListItem $item, ): string { if ($item is Blocks\TaskListItemExtension) { return $this->renderTaskListItemExtension($list, $item); } $children = $item->getChildren(); if (C\is_empty($children)) { return "<li></li>\n"; } $content = ''; if ($list->isTight()) { if (!C\first($children) is Blocks\Paragraph) { $content .= "\n"; } $content .= $children |> Vec\map( $$, $child ==> { if ($child is Blocks\Paragraph) { return $this->renderNodes($child->getContents()); } if ($child is Blocks\Block) { return Str\trim($this->render($child)); } return $this->render($child); }, ) |> Str\join($$, "\n"); if (!C\last($children) is Blocks\Paragraph) { $content .= "\n"; } } else { $content = "\n".$this->renderNodes($children); } return '<li>'.$content."</li>\n"; } <<__Override>> protected function renderListOfItems(Blocks\ListOfItems $node): string { $start = $node->getFirstNumber(); if ($start === null) { $start = '<ul>'; $end = '</ul>'; } else if ($start === 1) { $start = '<ol>'; $end = '</ol>'; } else { $start = \sprintf('<ol start="%d">', $start); $end = '</ol>'; } return $node->getItems() |> Vec\map($$, $item ==> $this->renderListItem($node, $item)) |> Str\join($$, '') |> $start."\n".$$.$end."\n"; } <<__Override>> protected function renderParagraph(Blocks\Paragraph $node): string { return '<p>'.$this->renderNodes($node->getContents())."</p>\n"; } <<__Override>> protected function renderTableExtension(Blocks\TableExtension $node): string { $html = "<table>\n".$this->renderTableHeader($node); $data = $node->getData(); if (C\is_empty($data)) { return $html."</table>\n"; } $html .= "\n<tbody>"; $row_idx = -1; foreach ($data as $row) { ++$row_idx; $html .= "\n".$this->renderTableDataRow($node, $row_idx, $row); } return $html."</tbody></table>\n"; } protected function renderTableHeader(Blocks\TableExtension $node): string { $html = "<thead>\n<tr>\n"; $alignments = $node->getColumnAlignments(); $header = $node->getHeader(); for ($i = 0; $i < C\count($header); ++$i) { $cell = $header[$i]; $alignment = $alignments[$i]; if ($alignment !== null) { $alignment = ' align="'.$alignment.'"'; } $html .= '<th'.($alignment ?? '').'>'.$this->renderNodes($cell)."</th>\n"; } $html .= "</tr>\n</thead>"; return $html; } protected function renderTableDataRow( Blocks\TableExtension $table, int $row_idx, Blocks\TableExtension::TRow $row, ): string { $html = "<tr>"; for ($i = 0; $i < C\count($row); ++$i) { $cell = $row[$i]; $html .= "\n".$this->renderTableDataCell($table, $row_idx, $i, $cell); } $html .= "\n</tr>"; return $html; } protected function renderTableDataCell( Blocks\TableExtension $table, int $_row_idx, int $col_idx, Blocks\TableExtension::TCell $cell, ): string { $alignment = $table->getColumnAlignments()[$col_idx]; if ($alignment !== null) { $alignment = ' align="'.$alignment.'"'; } return "<td".($alignment ?? '').'>'.$this->renderNodes($cell)."</td>"; } <<__Override>> protected function renderThematicBreak(): string { return "<hr />\n"; } <<__Override>> protected function renderAutoLink(Inlines\AutoLink $node): string { $href = self::escapeURIAttribute($node->getDestination()); $text = self::escapeContent($node->getText()); $noFollowUgcTag = $this->getContext()->areLinksNoFollowUGC() ? ' rel="nofollow ugc"' : ''; return '<a href="'.$href.'"'.$noFollowUgcTag.'>'.$text.'</a>'; } <<__Override>> protected function renderInlineWithPlainTextContent( Inlines\InlineWithPlainTextContent $node, ): string { return self::escapeContent($node->getContent()); } <<__Override>> protected function renderCodeSpan(Inlines\CodeSpan $node): string { return '<code>'.self::escapeContent($node->getCode()).'</code>'; } <<__Override>> protected function renderEmphasis(Inlines\Emphasis $node): string { $tag = $node->isStrong() ? 'strong' : 'em'; return $node->getContent() |> Vec\map($$, $item ==> $this->render($item)) |> Str\join($$, '') |> '<'.$tag.'>'.$$.'</'.$tag.'>'; } <<__Override>> protected function renderHardLineBreak(): string { return "<br />\n"; } <<__Override>> protected function renderImage(Inlines\Image $node): string { $title = $node->getTitle(); if ($title !== null) { $title = ' title="'.self::escapeAttribute($title).'"'; } $src = self::escapeURIAttribute($node->getSource()); $text = $node->getDescription() |> Vec\map($$, $child ==> $child->getContentAsPlainText()) |> Str\join($$, ''); // Needs to always be present for spec tests to pass $alt = ' alt="'.self::escapeAttribute($text).'"'; return '<img src="'.$src.'"'.$alt.($title ?? '').' />'; } <<__Override>> protected function renderLink(Inlines\Link $node): string { $title = $node->getTitle(); if ($title !== null) { $title = ' title="'.self::escapeAttribute($title).'"'; } $href = self::escapeURIAttribute($node->getDestination()); $text = $node->getText() |> Vec\map($$, $child ==> $this->render($child)) |> Str\join($$, ''); $noFollowUgcTag = $this->getContext()->areLinksNoFollowUGC() ? ' rel="nofollow ugc"' : ''; return '<a href="'.$href.'"'.$noFollowUgcTag."".($title ?? '').'>'.$text.'</a>'; } <<__Override>> protected function renderRawHTML(Inlines\RawHTML $node): string { return $node->getContent(); } <<__Override>> protected function renderSoftLineBreak(): string { return "\n"; } <<__Override>> protected function renderStrikethroughExtension( Inlines\StrikethroughExtension $node, ): string { $children = $node->getChildren() |> Vec\map($$, $child ==> $this->render($child)) |> Str\join($$, ''); return '<del>'.$children.'</del>'; } }