12 changed files with 711 additions and 0 deletions
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
<?php |
||||
|
||||
namespace App\Controller; |
||||
|
||||
use App\Entity\Article; |
||||
use App\Entity\User; |
||||
use App\Util\CommonMark\Converter; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use Psr\Cache\CacheItemPoolInterface; |
||||
use Psr\Cache\InvalidArgumentException; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\Attribute\Route; |
||||
|
||||
class ArticleController extends AbstractController |
||||
{ |
||||
/** |
||||
* @throws InvalidArgumentException |
||||
*/ |
||||
#[Route('/article/d/{slug}', name: 'article-slug')] |
||||
public function article(EntityManagerInterface $entityManager, CacheItemPoolInterface $articlesCache, |
||||
Converter $converter, $slug): Response |
||||
{ |
||||
$article = null; |
||||
// check if an item with same eventId already exists in the db |
||||
$repository = $entityManager->getRepository(Article::class); |
||||
$articles = $repository->findBy(['slug' => $slug]); |
||||
$revisions = count($repository->findBy(['slug' => $slug])); |
||||
|
||||
if ($revisions > 1) { |
||||
// sort articles by created at date |
||||
usort($articles, function ($a, $b) { |
||||
return $b->getCreatedAt() <=> $a->getCreatedAt(); |
||||
}); |
||||
// get the last article |
||||
$article = end($articles); |
||||
} else { |
||||
$article = $articles[0]; |
||||
} |
||||
|
||||
if (!$article) { |
||||
throw $this->createNotFoundException('The article does not exist'); |
||||
} |
||||
|
||||
$cacheKey = 'article_' . $article->getId(); |
||||
$cacheItem = $articlesCache->getItem($cacheKey); |
||||
if (!$cacheItem->isHit()) { |
||||
$cacheItem->set($converter->convertToHtml($article->getContent())); |
||||
$articlesCache->save($cacheItem); |
||||
} |
||||
|
||||
// find user by npub |
||||
$author = $entityManager->getRepository(User::class)->findOneBy(['npub' => $article->getPubkey()]); |
||||
|
||||
return $this->render('Pages/article.html.twig', [ |
||||
'article' => $article, |
||||
'author' => $author, |
||||
'content' => $cacheItem->get() |
||||
]); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\Bech32; |
||||
|
||||
use Exception; |
||||
|
||||
class Bech32Decoder |
||||
{ |
||||
// Function to decode Bech32 without character length restriction |
||||
/** |
||||
* @throws Exception |
||||
*/ |
||||
public function decodeBech32(string $bech32): array |
||||
{ |
||||
$charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; |
||||
|
||||
// Find the separator (1) |
||||
$pos = strrpos($bech32, '1'); |
||||
if ($pos === false) { |
||||
throw new Exception('Invalid Bech32 string'); |
||||
} |
||||
|
||||
// Extract human-readable part (HRP) |
||||
$hrp = substr($bech32, 0, $pos); |
||||
|
||||
// Extract data part |
||||
$data_part = substr($bech32, $pos + 1); |
||||
$data = []; |
||||
for ($i = 0; $i < strlen($data_part); $i++) { |
||||
$data[] = strpos($charset, $data_part[$i]); |
||||
if ($data[$i] === false) { |
||||
throw new Exception('Invalid character in Bech32 string'); |
||||
} |
||||
} |
||||
|
||||
return [$hrp, $data]; |
||||
} |
||||
|
||||
// Function to convert 5-bit data to 8-bit data |
||||
public function convertBits(array $data, int $fromBits, int $toBits, bool $pad = true): array |
||||
{ |
||||
$acc = 0; |
||||
$bits = 0; |
||||
$ret = []; |
||||
$maxv = (1 << $toBits) - 1; |
||||
$max_acc = (1 << ($fromBits + $toBits - 1)) - 1; |
||||
|
||||
foreach ($data as $value) { |
||||
if ($value < 0 || $value >> $fromBits) { |
||||
throw new Exception('Invalid value in data'); |
||||
} |
||||
$acc = (($acc << $fromBits) | $value) & $max_acc; |
||||
$bits += $fromBits; |
||||
while ($bits >= $toBits) { |
||||
$bits -= $toBits; |
||||
$ret[] = ($acc >> $bits) & $maxv; |
||||
} |
||||
} |
||||
|
||||
if ($pad) { |
||||
if ($bits > 0) { |
||||
$ret[] = ($acc << ($toBits - $bits)) & $maxv; |
||||
} |
||||
} else if ($bits >= $fromBits || (($acc << ($toBits - $bits)) & $maxv)) { |
||||
throw new Exception('Invalid padding'); |
||||
} |
||||
|
||||
return $ret; |
||||
} |
||||
|
||||
// Public method to decode a Nostr Bech32 string and return hex |
||||
public function decodeNostrBech32ToHex(string $bech32): string |
||||
{ |
||||
list($hrp, $data) = $this->decodeBech32($bech32); |
||||
|
||||
// Convert 5-bit data back to 8-bit data |
||||
$decodedData = $this->convertBits($data, 5, 8, false); |
||||
|
||||
// Return the decoded data as a hex string |
||||
return bin2hex(pack('C*', ...$decodedData)); |
||||
} |
||||
|
||||
// Public method to decode a Nostr Bech32 string and return the binary data |
||||
|
||||
/** |
||||
* @throws Exception |
||||
*/ |
||||
public function decodeNostrBech32ToBinary(string $bech32): array |
||||
{ |
||||
list($hrp, $data) = $this->decodeBech32($bech32); |
||||
|
||||
// Convert 5-bit data to 8-bit data |
||||
$decodedData = $this->convertBits($data, 5, 8); |
||||
|
||||
return [$hrp, $decodedData]; |
||||
} |
||||
|
||||
// Public method to parse the binary data into TLV format |
||||
public function parseTLV(array $binaryData): array |
||||
{ |
||||
$parsedTLVs = []; |
||||
$offset = 0; |
||||
|
||||
while ($offset < count($binaryData)) { |
||||
if ($offset + 1 >= count($binaryData)) { |
||||
throw new Exception("Incomplete TLV data"); |
||||
} |
||||
|
||||
// Read the Type (T) and Length (L) |
||||
$type = $binaryData[$offset]; |
||||
$length = $binaryData[$offset + 1]; |
||||
$offset += 2; |
||||
|
||||
// Ensure we have enough data for the value |
||||
if ($offset + $length > count($binaryData)) { |
||||
break; |
||||
} else { |
||||
// Extract the Value (V) |
||||
$value = array_slice($binaryData, $offset, $length); |
||||
} |
||||
|
||||
$offset += $length; |
||||
|
||||
// Add the TLV to the parsed array |
||||
$parsedTLVs[] = [ |
||||
'type' => $type, |
||||
'length' => $length, |
||||
'value' => $value, |
||||
]; |
||||
} |
||||
|
||||
return $parsedTLVs; |
||||
} |
||||
|
||||
// Decode and parse a Bech32 string |
||||
|
||||
/** |
||||
* @throws Exception |
||||
*/ |
||||
public function decodeAndParseNostrBech32(string $bech32): array |
||||
{ |
||||
// Step 1: Decode Bech32 to binary data |
||||
list($hrp, $binaryData) = $this->decodeNostrBech32ToBinary($bech32); |
||||
|
||||
if ($hrp == 'npub') { |
||||
return [$hrp, $binaryData]; |
||||
} |
||||
|
||||
// Step 2: Parse the binary data into TLV format |
||||
$tlvData = $this->parseTLV($binaryData); |
||||
|
||||
return [$hrp, $tlvData]; |
||||
} |
||||
} |
||||
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark; |
||||
|
||||
use App\Util\Bech32\Bech32Decoder; |
||||
use App\Util\CommonMark\NostrSchemeExtension\NostrSchemeExtension; |
||||
use League\CommonMark\Environment\Environment; |
||||
use League\CommonMark\Exception\CommonMarkException; |
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension; |
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; |
||||
use League\CommonMark\Extension\Footnote\FootnoteExtension; |
||||
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension; |
||||
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; |
||||
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; |
||||
use League\CommonMark\Extension\Table\TableExtension; |
||||
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension; |
||||
use League\CommonMark\MarkdownConverter; |
||||
|
||||
class Converter |
||||
{ |
||||
public function __construct(private readonly Bech32Decoder $bech32Decoder) |
||||
{ |
||||
} |
||||
|
||||
/** |
||||
* @throws CommonMarkException |
||||
*/ |
||||
public function convertToHTML(string $markdown): string |
||||
{ |
||||
// Check if the article has more than three headings |
||||
// Match all headings (from level 1 to 6) |
||||
preg_match_all('/^#+\s.*$/m', $markdown, $matches); |
||||
$headingsCount = count($matches[0]); |
||||
|
||||
|
||||
// Configure the Environment with all the CommonMark parsers/renderers |
||||
$config = [ |
||||
'table_of_contents' => [ |
||||
'min_heading_level' => 1, |
||||
'max_heading_level' => 2, |
||||
], |
||||
'heading_permalink' => [ |
||||
'symbol' => '§', |
||||
], |
||||
'autolink' => [ |
||||
'allowed_protocols' => ['https'], // defaults to ['https', 'http', 'ftp'] |
||||
'default_protocol' => 'https', // defaults to 'http' |
||||
], |
||||
]; |
||||
$environment = new Environment($config); |
||||
// Add the extensions |
||||
$environment->addExtension(new CommonMarkCoreExtension()); |
||||
$environment->addExtension(new FootnoteExtension()); |
||||
$environment->addExtension(new TableExtension()); |
||||
$environment->addExtension(new StrikethroughExtension()); |
||||
// create a custom extension, that handles nostr mentions |
||||
$environment->addExtension(new NostrSchemeExtension($this->bech32Decoder)); |
||||
$environment->addExtension(new SmartPunctExtension()); |
||||
$environment->addExtension(new AutolinkExtension()); |
||||
if ($headingsCount > 3) { |
||||
$environment->addExtension(new HeadingPermalinkExtension()); |
||||
$environment->addExtension(new TableOfContentsExtension()); |
||||
} |
||||
|
||||
// Instantiate the converter engine and start converting some Markdown! |
||||
$converter = new MarkdownConverter($environment); |
||||
$content = html_entity_decode($markdown); |
||||
|
||||
dump($content); |
||||
|
||||
return $converter->convert($content); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Node\Node; |
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface; |
||||
use League\CommonMark\Renderer\NodeRendererInterface; |
||||
use League\CommonMark\Util\HtmlElement; |
||||
|
||||
class NostrEventRenderer implements NodeRendererInterface |
||||
{ |
||||
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer) |
||||
{ |
||||
if (!($node instanceof NostrSchemeData)) { |
||||
throw new \InvalidArgumentException('Incompatible inline node type: ' . get_class($node)); |
||||
} |
||||
|
||||
if ($node->getType() === 'nevent') { |
||||
// Construct the local link URL from the special part |
||||
$url = '/e/' . $node->getSpecial(); |
||||
} else if ($node->getType() === 'naddr') { |
||||
dump($node); |
||||
// Construct the local link URL from the special part |
||||
$url = '/' . $node->getSpecial(); |
||||
} |
||||
|
||||
if (isset($url)) { |
||||
// Create the anchor element |
||||
return new HtmlElement('a', ['href' => $url], '@' . $node->getSpecial()); |
||||
} |
||||
|
||||
return false; |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Node\Inline\AbstractInline; |
||||
|
||||
class NostrMentionLink extends AbstractInline |
||||
{ |
||||
private ?string $label; |
||||
private string $npub; |
||||
|
||||
public function __construct(?string $label, string $npubPart) |
||||
{ |
||||
parent::__construct(); |
||||
|
||||
$this->label = $label; |
||||
$this->npub = $npubPart; |
||||
} |
||||
|
||||
public function getLabel(): ?string |
||||
{ |
||||
return $this->label; |
||||
} |
||||
|
||||
public function getNpub(): string |
||||
{ |
||||
return $this->npub; |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface; |
||||
use League\CommonMark\Parser\Inline\InlineParserMatch; |
||||
use League\CommonMark\Parser\InlineParserContext; |
||||
use swentel\nostr\Key\Key; |
||||
|
||||
/** |
||||
* Class NostrMentionParser |
||||
* Looks for links that look like Markdown links in the format `[label](url)`, |
||||
* but have npub1XXXX instead of a URL |
||||
* @package App\Util\CommonMark |
||||
*/ |
||||
class NostrMentionParser implements InlineParserInterface |
||||
{ |
||||
|
||||
public function getMatchDefinition(): InlineParserMatch |
||||
{ |
||||
// Define a match for a markdown link-like structure with "npub" links |
||||
return InlineParserMatch::regex('\[([^\]]+)\]\(npub1[0-9a-zA-Z]+\)'); |
||||
} |
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool |
||||
{ |
||||
$cursor = $inlineContext->getCursor(); |
||||
// Get the match and extract relevant parts |
||||
$matches = $inlineContext->getMatches(); |
||||
|
||||
// The entire match is like "[label](npubXXXX)", now we need to extract "label" and "npubXXXX" |
||||
$fullMatch = $matches[0]; // Full matched string like "[label](npubXXXX)" |
||||
$label = $matches[1]; // This is the text between the square brackets: "label" |
||||
|
||||
// Extract "npub" part from fullMatch |
||||
$npubLink = substr($fullMatch, strpos($fullMatch, 'npub1'), -1); // e.g., "npubXXXX" |
||||
$npubPart = substr($npubLink, 5); // Extract the part after "npub1", i.e., "XXXX" |
||||
|
||||
$key = new Key(); |
||||
$hex = $key->convertToHex($npubLink); |
||||
|
||||
// Create a new inline node for the custom link |
||||
$inlineContext->getContainer()->appendChild(new NostrMentionLink($label, $hex)); |
||||
|
||||
// Advance the cursor to consume the matched part (important!) |
||||
$cursor->advanceBy(strlen($fullMatch)); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Node\Node; |
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface; |
||||
use League\CommonMark\Renderer\NodeRendererInterface; |
||||
use League\CommonMark\Util\HtmlElement; |
||||
|
||||
class NostrMentionRenderer implements NodeRendererInterface |
||||
{ |
||||
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): HtmlElement |
||||
{ |
||||
if (!($node instanceof NostrMentionLink)) { |
||||
throw new \InvalidArgumentException('Incompatible inline node type: ' . get_class($node)); |
||||
} |
||||
|
||||
$label = $node->getLabel() ?? $this->labelFromKey($node->getNpub()); |
||||
|
||||
// Construct the local link URL from the npub part |
||||
$url = '/p/' . $node->getNpub(); |
||||
|
||||
// Create the anchor element |
||||
return new HtmlElement('a', ['href' => $url], '@' . $label); |
||||
} |
||||
|
||||
private function labelFromKey($npub): string |
||||
{ |
||||
$start = substr($npub, 0, 5); // First 5 characters |
||||
$end = substr($npub, -5); // Last 5 characters |
||||
return $start . '...' . $end; // Concatenate with ellipsis |
||||
} |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface; |
||||
use League\CommonMark\Parser\Inline\InlineParserMatch; |
||||
use League\CommonMark\Parser\InlineParserContext; |
||||
use swentel\nostr\Key\Key; |
||||
|
||||
/** |
||||
* Class NostrRawNpubParser |
||||
* Looks for raw nostr mentions formatted as npub1XXXX |
||||
*/ |
||||
class NostrRawNpubParser implements InlineParserInterface |
||||
{ |
||||
|
||||
public function __construct() |
||||
{ |
||||
} |
||||
|
||||
public function getMatchDefinition(): InlineParserMatch |
||||
{ |
||||
return InlineParserMatch::regex('npub1[0-9a-zA-Z]+'); |
||||
} |
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool |
||||
{ |
||||
$cursor = $inlineContext->getCursor(); |
||||
// Get the match and extract relevant parts |
||||
$fullMatch = $inlineContext->getFullMatch(); |
||||
$key = new Key(); |
||||
$hex = $key->convertToHex($fullMatch); |
||||
|
||||
// Create a new inline node for the custom link |
||||
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $hex)); |
||||
|
||||
// Advance the cursor to consume the matched part (important!) |
||||
$cursor->advanceBy(strlen($fullMatch)); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use League\CommonMark\Node\Inline\AbstractInline; |
||||
|
||||
/** |
||||
* Class NostrSchemeData |
||||
* NIP-19 bech32-encoded entities |
||||
* |
||||
* @package App\Util\CommonMark |
||||
*/ |
||||
class NostrSchemeData extends AbstractInline |
||||
{ |
||||
private $type; |
||||
private $special; |
||||
private $relays; |
||||
private $author; |
||||
private $kind; |
||||
|
||||
public function __construct($type, $special, $relays, $author, $kind) |
||||
{ |
||||
parent::__construct(); |
||||
|
||||
$this->type = $type; |
||||
$this->special = $special; |
||||
$this->relays = $relays; |
||||
$this->author = $author; |
||||
$this->kind = $kind; |
||||
} |
||||
|
||||
public function getType() |
||||
{ |
||||
return $this->type; |
||||
} |
||||
|
||||
public function getSpecial() |
||||
{ |
||||
return $this->special; |
||||
} |
||||
|
||||
public function getRelays() |
||||
{ |
||||
return $this->relays; |
||||
} |
||||
|
||||
public function getAuthor() |
||||
{ |
||||
return $this->author; |
||||
} |
||||
|
||||
public function getKind() |
||||
{ |
||||
return $this->kind; |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use App\Util\Bech32\Bech32Decoder; |
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface; |
||||
use League\CommonMark\Extension\ExtensionInterface; |
||||
|
||||
class NostrSchemeExtension implements ExtensionInterface |
||||
{ |
||||
|
||||
public function __construct(private readonly Bech32Decoder $bech32Decoder) |
||||
{ |
||||
} |
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void |
||||
{ |
||||
$environment |
||||
->addInlineParser(new NostrMentionParser(), 200) |
||||
->addInlineParser(new NostrSchemeParser($this->bech32Decoder), 199) |
||||
->addInlineParser(new NostrRawNpubParser(), 198) |
||||
|
||||
->addRenderer(NostrSchemeData::class, new NostrEventRenderer(), 2) |
||||
->addRenderer(NostrMentionLink::class, new NostrMentionRenderer(), 1) |
||||
; |
||||
} |
||||
} |
||||
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
<?php |
||||
|
||||
namespace App\Util\CommonMark\NostrSchemeExtension; |
||||
|
||||
use App\Util\Bech32\Bech32Decoder; |
||||
use League\CommonMark\Parser\Inline\InlineParserInterface; |
||||
use League\CommonMark\Parser\Inline\InlineParserMatch; |
||||
use League\CommonMark\Parser\InlineParserContext; |
||||
use function BitWasp\Bech32\convertBits; |
||||
use function BitWasp\Bech32\decode; |
||||
|
||||
|
||||
class NostrSchemeParser implements InlineParserInterface |
||||
{ |
||||
|
||||
public function __construct(private Bech32Decoder $bech32Decoder) |
||||
{ |
||||
} |
||||
|
||||
public function getMatchDefinition(): InlineParserMatch |
||||
{ |
||||
return InlineParserMatch::regex('nostr:[0-9a-zA-Z]+'); |
||||
} |
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool |
||||
{ |
||||
$cursor = $inlineContext->getCursor(); |
||||
// Get the match and extract relevant parts |
||||
$fullMatch = $inlineContext->getFullMatch(); |
||||
// The match is a Bech32 encoded string |
||||
// decode it to get the parts |
||||
$bechEncoded = substr($fullMatch, 6); // Extract the part after "nostr:", i.e., "XXXX" |
||||
dump($bechEncoded); |
||||
|
||||
try { |
||||
list($hrp, $tlv) = $this->bech32Decoder->decodeAndParseNostrBech32($bechEncoded); |
||||
dump($hrp); |
||||
dump($tlv); |
||||
switch ($hrp) { |
||||
case 'npub': |
||||
$str = ''; |
||||
list($hrp, $data) = decode($bechEncoded); |
||||
$bytes = convertBits($data, count($data), 5, 8, false); |
||||
foreach ($bytes as $item) { |
||||
$str .= str_pad(dechex($item), 2, '0', STR_PAD_LEFT); |
||||
} |
||||
$npubPart = $str; |
||||
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $npubPart)); |
||||
break; |
||||
case 'nprofile': |
||||
$type = 0; // npub |
||||
foreach ($tlv as $item) { |
||||
if ($item['type'] === $type) { |
||||
// from array of integers to string |
||||
$str = ''; |
||||
foreach ($item['value'] as $byte) { |
||||
$str .= str_pad(dechex($byte), 2, '0', STR_PAD_LEFT); |
||||
} |
||||
$npubPart = $str; |
||||
break; |
||||
} |
||||
} |
||||
if (isset($npubPart)) { |
||||
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $npubPart)); |
||||
} |
||||
break; |
||||
case 'nevent': |
||||
foreach ($tlv as $item) { |
||||
// event id |
||||
if ($item['type'] === 0) { |
||||
$eventId = implode('', array_map(fn($byte) => sprintf('%02x', $byte), $item['value'])); |
||||
break; |
||||
} |
||||
// relays |
||||
if ($item['type'] === 1) { |
||||
$relays[] = implode('', array_map('chr', $item['value'])); |
||||
} |
||||
} |
||||
dump($relays ?? null); |
||||
// TODO also potentially contains relays, author, and kind |
||||
$inlineContext->getContainer()->appendChild(new NostrSchemeData('nevent', $eventId, $relays ?? null, null, null)); |
||||
break; |
||||
case 'naddr': |
||||
$inlineContext->getContainer()->appendChild(new NostrSchemeData('naddr', $bechEncoded, null, null, null)); |
||||
break; |
||||
case 'nrelay': |
||||
// deprecated |
||||
default: |
||||
return false; |
||||
} |
||||
|
||||
} catch (\Exception $e) { |
||||
dump($e->getMessage()); |
||||
return false; |
||||
} |
||||
|
||||
// Advance the cursor to consume the matched part (important!) |
||||
$cursor->advanceBy(strlen($fullMatch)); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block body %} |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<h1 class="card-title">{{ article.title }}</h1> |
||||
</div> |
||||
{% if author %} |
||||
<div class="byline"> |
||||
<span> |
||||
{{ 'text.byline'|trans }} <a href="{{ path('author', {'npub': author.npub}) }}"> |
||||
<twig:NameOrNpub displayName="{{ author.displayName }}" name="{{ author.name }}" npub="{{ author.npub }}" /> |
||||
</a> |
||||
</span> |
||||
<span> |
||||
<small><twig:ux:icon name="heroicons:pencil" class="icon" /> {{ article.createdAt|date('F j, Y') }}</small><br> |
||||
{% if article.publishedAt is not null %}<small>{{ article.publishedAt|date('F j, Y') }}</small>{% endif %} |
||||
</span> |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
<div class="card-body"> |
||||
<div class="lede"> |
||||
{{ article.summary }} |
||||
</div> |
||||
|
||||
{% if article.image %} |
||||
<div class="article__image"> |
||||
<img src="{{ article.image }}" alt="{{ article.title }}"> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<div class="article-main"> |
||||
{{ content|raw }} |
||||
</div> |
||||
|
||||
<hr class="divider" /> |
||||
<div class="tags"> |
||||
{% for tag in article.topics %} |
||||
<span class="tag">{{ tag }}</span> |
||||
{% endfor %} |
||||
</div> |
||||
|
||||
</div> |
||||
{% endblock %} |
||||
Loading…
Reference in new issue