diff --git a/assets/app.js b/assets/app.js
index 5c8e97e..196585b 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -12,6 +12,7 @@ import './styles/layout.css';
import './styles/button.css';
import './styles/card.css';
import './styles/article.css';
+import './styles/og.css';
import './styles/form.css';
import './styles/notice.css';
import './styles/spinner.css';
diff --git a/assets/controllers/nostr_preview_controller.js b/assets/controllers/nostr_preview_controller.js
index 09d4348..b69d88b 100644
--- a/assets/controllers/nostr_preview_controller.js
+++ b/assets/controllers/nostr_preview_controller.js
@@ -16,34 +16,56 @@ export default class extends Controller {
async fetchPreview() {
try {
- // Show loading indicator
this.containerTarget.innerHTML = '
No URL provided.
', 400);
+ }
+ try {
+ $embed = new \Embed\Embed();
+ $info = $embed->get($url);
+ if (!$info) {
+ throw new \Exception('No OG data found');
+ }
+ return $this->render('components/Molecules/OgPreview.html.twig', [
+ 'og' => [
+ 'title' => $info->title,
+ 'description' => $info->description,
+ 'image' => $info->image,
+ 'url' => $url
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return new Response('Unable to load OG preview for ' . htmlspecialchars($url) . '
', 200);
+ }
+ }
}
diff --git a/src/Service/NostrLinkParser.php b/src/Service/NostrLinkParser.php
index 1f82bd6..62e3974 100644
--- a/src/Service/NostrLinkParser.php
+++ b/src/Service/NostrLinkParser.php
@@ -7,8 +7,9 @@ use Psr\Log\LoggerInterface;
readonly class NostrLinkParser
{
- private const string NOSTR_LINK_PATTERN = '/(?:nostr:)?(nevent1[a-z0-9]+|naddr1[a-z0-9]+|nprofile1[a-z0-9]+|note1[a-z0-9]+|npub1[a-z0-9]+)/';
+ private const string NOSTR_LINK_PATTERN = '/(?:nostr:)(nevent1[a-z0-9]+|naddr1[a-z0-9]+|nprofile1[a-z0-9]+|note1[a-z0-9]+|npub1[a-z0-9]+)/';
+ private const URL_PATTERN = '/https?:\/\/[\w\-\.\?\,\'\/\\\+&%@\?\$#_=:\(\)~;]+/i';
public function __construct(
private LoggerInterface $logger
@@ -23,36 +24,95 @@ readonly class NostrLinkParser
public function parseLinks(string $content): array
{
$links = [];
+ $links = array_merge(
+ $this->parseUrlsWithNostrIds($content),
+ $this->parseBareNostrIdentifiers($content)
+ );
+ // Sort by position to maintain the original order in the text
+ usort($links, fn($a, $b) => $a['position'] <=> $b['position']);
+ return $links;
+ }
+
+ private function parseUrlsWithNostrIds(string $content): array
+ {
+ $links = [];
+ if (preg_match_all(self::URL_PATTERN, $content, $urlMatches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ foreach ($urlMatches as $urlMatch) {
+ $url = $urlMatch[0][0];
+ $position = $urlMatch[0][1];
+ $nostrId = null;
+ $nostrType = null;
+ $nostrData = null;
+ if (preg_match(self::NOSTR_LINK_PATTERN, $url, $nostrMatch)) {
+ $nostrId = $nostrMatch[1];
+ try {
+ $decoded = new Bech32($nostrId);
+ $nostrType = $decoded->type;
+ $nostrData = $decoded->data;
+ } catch (\Exception $e) {
+ $this->logger->info('Failed to decode Nostr identifier in URL', [
+ 'identifier' => $nostrId,
+ 'error' => $e->getMessage()
+ ]);
+ }
+ }
+ $links[] = [
+ 'type' => $nostrType ?? 'url',
+ 'identifier' => $nostrId,
+ 'full_match' => $url,
+ 'position' => $position,
+ 'data' => $nostrData,
+ 'is_url' => true
+ ];
+ }
+ }
+ return $links;
+ }
+
+ private function parseBareNostrIdentifiers(string $content): array
+ {
+ $links = [];
+
- // Improved regular expression to match all nostr: links
- // This will find all occurrences including multiple links in the same text
if (preg_match_all(self::NOSTR_LINK_PATTERN, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+
+ // If match starts with nostr:, continue otherwise check if part of URL
+ if (!(str_starts_with($matches[0][0][0], 'nostr:'))) {
+ // Check if the match is part of a URL, as path or query parameter
+ $urlPattern = '/https?:\/\/[\w\-.?,\'\/+&%$#@_=:()~;]+/i';
+ foreach ($matches as $key => $match) {
+ $position = $match[0][1];
+ // Check if the match is preceded by a URL
+ $precedingContent = substr($content, 0, $position);
+
+ if (preg_match($urlPattern, $precedingContent)) {
+ // If the match is preceded by a URL, skip it
+ unset($matches[$key]);
+ }
+ }
+ }
+
foreach ($matches as $match) {
- $fullMatch = $match[0][0];
$identifier = $match[1][0];
- $position = $match[0][1]; // Position in the text
-
+ $position = $match[0][1];
+ // This check will be handled in parseLinks by sorting and merging
try {
$decoded = new Bech32($identifier);
$links[] = [
'type' => $decoded->type,
'identifier' => $identifier,
- 'full_match' => $fullMatch,
+ 'full_match' => $match[0][0],
'position' => $position,
- 'data' => $decoded->data
+ 'data' => $decoded->data,
+ 'is_url' => false
];
} catch (\Exception $e) {
- // If decoding fails, skip this identifier
$this->logger->info('Failed to decode Nostr identifier', [
'identifier' => $identifier,
'error' => $e->getMessage()
]);
- continue;
}
}
-
- // Sort by position to maintain the original order in the text
- usort($links, fn($a, $b) => $a['position'] <=> $b['position']);
}
return $links;
diff --git a/templates/components/Molecules/Card.html.twig b/templates/components/Molecules/Card.html.twig
index 65d36a7..2f8306b 100644
--- a/templates/components/Molecules/Card.html.twig
+++ b/templates/components/Molecules/Card.html.twig
@@ -17,9 +17,11 @@
{{ article.title }}
-
- {{ article.summary }}
-
+ {% if article.summary %}
+
+ {{ article.summary }}
+
+ {% endif %}
diff --git a/templates/components/Molecules/NostrPreview.html.twig b/templates/components/Molecules/NostrPreview.html.twig
index 12ca3fd..bf49ef3 100644
--- a/templates/components/Molecules/NostrPreview.html.twig
+++ b/templates/components/Molecules/NostrPreview.html.twig
@@ -9,5 +9,19 @@
+ {# Example: show kind if available #}
+ {% if preview.data.kind is defined %}
+ Kind: {{ preview.data.kind }}
+ {% endif %}
+ {# Add more event details here as needed #}
+
+ {% endif %}
diff --git a/templates/components/Molecules/OgPreview.html.twig b/templates/components/Molecules/OgPreview.html.twig
new file mode 100644
index 0000000..72d2ed6
--- /dev/null
+++ b/templates/components/Molecules/OgPreview.html.twig
@@ -0,0 +1,21 @@
+