src/inlines/Link.php (228 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\Inlines;
use function Facebook\Markdown\_Private\{
consume_link_destination,
consume_link_title,
};
use const Facebook\Markdown\_Private\ASCII_PUNCTUATION;
use namespace Facebook\Markdown\Inlines\_Private\StrPos;
use type Facebook\Markdown\UnparsedBlocks\LinkReferenceDefinition;
use namespace HH\Lib\{C, Str, Vec};
class Link extends Inline {
public function __construct(
private vec<Inline> $text,
private string $destination,
private ?string $title,
) {
}
<<__Override>>
public function getContentAsPlainText(): string {
return $this->text
|> Vec\map($$, $child ==> $child->getContentAsPlainText())
|> Str\join($$, '');
}
public function getText(): vec<Inline> {
return $this->text;
}
public function getDestination(): string {
return $this->destination;
}
public function getTitle(): ?string {
return $this->title;
}
<<__Override>>
public static function consume(
Context $ctx,
string $string,
int $offset,
): ?(Link, int) {
return self::consumeLinkish(
$ctx,
$string,
$offset,
keyset[
CodeSpan::class,
AutoLink::class,
RawHTML::class,
],
);
}
public static function consumeLinkish(
Context $ctx,
string $string,
int $offset,
keyset<classname<Inline>> $inners,
): ?(Link, int) {
if ($string[$offset] !== '[') {
return null;
}
$offset++;
$depth = 1;
$len = Str\length($string);
$key = '';
$start = $offset;
for ($offset = $offset; $offset < $len; ++$offset) {
$chr = $string[$offset];
if ($chr === ']') {
--$depth;
if ($depth === 0) {
$offset;
break;
}
continue;
}
if ($chr === '[') {
if (
$string[$offset - 1] !== '!' &&
!C\contains_key($inners, Link::class) &&
self::consume($ctx, $string, $offset) !== null
) {
return null;
}
++$depth;
continue;
}
if ($chr === '\\') {
if ($offset + 1 < $len) {
$next = $string[$offset + 1];
if (C\contains_key(ASCII_PUNCTUATION, $next)) {
++$offset;
continue;
}
}
}
$result = null;
foreach ($inners as $type) {
$result = $type::consume($ctx, $string, $offset);
if ($result !== null) {
break;
}
}
if ($result !== null) {
list($_, $offset) = $result;
--$offset;
continue;
}
}
if ($depth !== 0) {
return null;
}
$key = Str\slice($string, $start, $offset - $start);
$offset++;
$text = parse($ctx, $key);
if (Str\slice($string, $offset, 2) === '[]') {
// collapsed reference link
$def = $ctx->getBlockContext()->getLinkReferenceDefinition($key);
if ($def === null) {
return null;
}
return tuple(
new self($text, $def->getDestination(), $def->getTitle()),
$offset + 2,
);
}
if ($offset < $len && $string[$offset] === '[') {
// full reference link
$depth = 1;
$matched = '';
$offset++;
for ($i = 0; $i < 999 && $offset < $len; ++$i, ++$offset) {
$char = $string[$offset];
if ($char === '[') {
++$depth;
$matched .= $char;
continue;
}
if ($char === ']') {
--$depth;
if ($depth === 0) {
break;
}
$matched .= $char;
continue;
}
if ($char === '\\') {
if ($offset + 1 >= $len) {
return null;
}
$matched .= $char.$string[$offset + 1];
++$offset;
continue;
}
$matched .= $char;
}
if ($depth !== 0) {
return null;
}
$key = LinkReferenceDefinition::normalizeKey($matched);
$def = $ctx->getBlockContext()->getLinkReferenceDefinition($key);
if ($def === null) {
return null;
}
return tuple(
new self($text, $def->getDestination(), $def->getTitle()),
$offset + 1,
);
}
$result = self::consumeDestinationAndTitle($string, $offset);
if ($result !== null) {
list($destination, $title, $offset) = $result;
if (!$ctx->areAllURISchemesEnabled()) {
$allowed_uri_schemes = $ctx->getAllowedURISchemes();
if (
!C\any(
$allowed_uri_schemes,
$elem ==> Str\starts_with_ci($destination, $elem.':'),
)
) {
return null;
}
}
return tuple(new self($text, $destination, $title), $offset);
}
// shortcut reference link?
$def = $ctx->getBlockContext()->getLinkReferenceDefinition($key);
if ($def === null) {
return null;
}
return tuple(
new self($text, $def->getDestination(), $def->getTitle()),
$offset,
);
}
private static function consumeDestinationAndTitle(
string $markdown,
int $offset,
): ?(string, ?string, int) {
$len = Str\length($markdown);
if ($offset === $len || $markdown[$offset] !== '(') {
return null;
}
$offset++;
$maybe_offset = StrPos\trim_left($markdown, $offset);
$slice = Str\slice($markdown, $maybe_offset);
$destination = consume_link_destination($slice);
if ($destination !== null) {
list($destination, $consumed) = $destination;
} else {
$destination = '';
$consumed = 0;
}
$offset = StrPos\trim_left($markdown, $maybe_offset + $consumed);
if ($offset === $len) {
return null;
}
$slice = Str\slice($markdown, $offset);
$title = consume_link_title($slice);
if ($title !== null) {
list($title, $consumed) = $title;
$offset = StrPos\trim_left($markdown, $offset + $consumed);
} else {
// make title ?markdown, instead of markdown|?(markdown, markdown)
$title = null;
}
if ($offset === $len) {
return null;
}
if ($markdown[$offset] !== ')') {
return null;
}
return tuple($destination, $title, $offset + 1);
}
}