Initial baseline: LaraBlog core with plugin commerce surface.
Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Blog;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* Attachment embeds:
|
||||
* - Legacy HTML: [attach=123] or src/href="[attach=123]"
|
||||
* - Markdown-safe:  or [label](attach:123)
|
||||
*
|
||||
* DB stores references only; URLs are resolved at render/preview time.
|
||||
*/
|
||||
class AttachEmbed
|
||||
{
|
||||
public const LEGACY_PATTERN = '/\[attach=(\d+)\]/i';
|
||||
|
||||
public const MD_PROTOCOL = 'attach:';
|
||||
|
||||
public function legacyToMarkdownReference(string $content, ?int $articleId = null): string
|
||||
{
|
||||
return preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use ($articleId): string {
|
||||
$id = (int) $matches[1];
|
||||
$attachment = $this->findAttachment($id, $articleId);
|
||||
|
||||
$name = $attachment?->filename ?: "attachment-{$id}";
|
||||
$alt = pathinfo($name, PATHINFO_FILENAME) ?: $name;
|
||||
|
||||
if ($attachment && $this->isImage($attachment)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '['.$this->escapeMdLabel($name).']('.self::MD_PROTOCOL.$id.')';
|
||||
}, $content) ?? $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protect sablog attach tokens before HTML→Markdown conversion.
|
||||
* Tokens avoid underscores so html-to-markdown won't escape them.
|
||||
*
|
||||
* @return array{0: string, 1: array<string, array{id:int, kind:string, role:string}>}
|
||||
*/
|
||||
public function protectLegacyTokensForHtmlConversion(string $html, ?int $articleId = null): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
// Attribute values become markdown link/image destinations → restore as attach:ID only.
|
||||
$html = preg_replace_callback(
|
||||
'/\b(src|href)=([\'"])\[attach=(\d+)\]\2/i',
|
||||
function (array $matches) use (&$map, $articleId): string {
|
||||
$id = (int) $matches[3];
|
||||
$role = 'dest';
|
||||
$kind = strtolower($matches[1]) === 'src' ? 'image' : 'file';
|
||||
$token = 'LBATTACHDEST'.$id.'X';
|
||||
$map[$token] = ['id' => $id, 'kind' => $kind, 'role' => $role, 'article_id' => $articleId];
|
||||
|
||||
return $matches[1].'='.$matches[2].$token.$matches[2];
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
|
||||
// Bare tokens become full markdown embeds after conversion.
|
||||
$html = preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use (&$map, $articleId): string {
|
||||
$id = (int) $matches[1];
|
||||
$attachment = $this->findAttachment($id, $articleId);
|
||||
$kind = ($attachment && $this->isImage($attachment)) ? 'image' : 'file';
|
||||
$token = 'LBATTACHBARE'.$id.'X';
|
||||
$map[$token] = ['id' => $id, 'kind' => $kind, 'role' => 'bare', 'article_id' => $articleId];
|
||||
|
||||
return $token;
|
||||
}, $html) ?? $html;
|
||||
|
||||
return [$html, $map];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{id:int, kind:string, role:string}> $map
|
||||
*/
|
||||
public function restoreProtectedTokensToMarkdown(string $markdown, array $map): string
|
||||
{
|
||||
foreach ($map as $token => $meta) {
|
||||
$id = (int) $meta['id'];
|
||||
|
||||
if (($meta['role'] ?? '') === 'dest') {
|
||||
$markdown = str_replace($token, self::MD_PROTOCOL.$id, $markdown);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachment = $this->findAttachment($id, $meta['article_id'] ?? null);
|
||||
$name = $attachment?->filename ?: "attachment-{$id}";
|
||||
$alt = pathinfo($name, PATHINFO_FILENAME) ?: $name;
|
||||
|
||||
$replacement = ($meta['kind'] ?? 'file') === 'image'
|
||||
? ''
|
||||
: '['.$this->escapeMdLabel($name).']('.self::MD_PROTOCOL.$id.')';
|
||||
|
||||
$markdown = str_replace($token, $replacement, $markdown);
|
||||
}
|
||||
|
||||
return $markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render-time only: turn attach:123 into /attachment.php?id=123 for CommonMark.
|
||||
*/
|
||||
public function expandMarkdownAttachProtocol(string $markdown): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/\]\(attach:(\d+)\)/i',
|
||||
fn (array $matches): string => ']('.$this->publicEntryUrl((int) $matches[1]).')',
|
||||
$markdown
|
||||
) ?? $markdown;
|
||||
}
|
||||
|
||||
public function hydrateHtml(string $html, ?int $articleId = null): string
|
||||
{
|
||||
// 1) Attribute form: src/href="[attach=N]" (must run before bare token replace)
|
||||
$html = preg_replace_callback(
|
||||
'/\b(src|href)=([\'"])\[attach=(\d+)\]\2/i',
|
||||
function (array $matches): string {
|
||||
$attr = strtolower($matches[1]);
|
||||
$quote = $matches[2];
|
||||
$id = (int) $matches[3];
|
||||
|
||||
return $attr.'='.$quote.$this->publicEntryUrl($id).$quote;
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
|
||||
// 2) Safety net for attach: protocol left in HTML attributes
|
||||
$html = preg_replace_callback(
|
||||
'/\b(href|src)=([\'"])attach:(\d+)\2/i',
|
||||
function (array $matches): string {
|
||||
$attr = strtolower($matches[1]);
|
||||
$quote = $matches[2];
|
||||
$id = (int) $matches[3];
|
||||
|
||||
return $attr.'='.$quote.$this->publicEntryUrl($id).$quote;
|
||||
},
|
||||
$html
|
||||
) ?? $html;
|
||||
|
||||
// 3) Bare legacy [attach=id] tokens
|
||||
$html = preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use ($articleId): string {
|
||||
$id = (int) $matches[1];
|
||||
$attachment = $this->findAttachment($id, $articleId);
|
||||
$url = $this->publicEntryUrl($id);
|
||||
|
||||
if ($attachment && $this->isImage($attachment)) {
|
||||
$alt = e(pathinfo($attachment->filename, PATHINFO_FILENAME) ?: $attachment->filename);
|
||||
|
||||
return '<img src="'.e($url).'" alt="'.$alt.'" />';
|
||||
}
|
||||
|
||||
$label = e($attachment?->filename ?: "attachment-{$id}");
|
||||
|
||||
return '<a href="'.e($url).'">'.$label.'</a>';
|
||||
}, $html) ?? $html;
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function publicEntryUrl(int $attachmentId): string
|
||||
{
|
||||
return url('/attachment.php?id='.$attachmentId);
|
||||
}
|
||||
|
||||
protected function findAttachment(int $id, ?int $articleId = null): ?Attachment
|
||||
{
|
||||
try {
|
||||
return Cache::remember("attach-embed:{$id}", 60, function () use ($id) {
|
||||
return Attachment::query()->find($id);
|
||||
});
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function isImage(Attachment $attachment): bool
|
||||
{
|
||||
if (is_string($attachment->mime) && str_starts_with($attachment->mime, 'image/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($attachment->filename, PATHINFO_EXTENSION));
|
||||
|
||||
return in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'], true);
|
||||
}
|
||||
|
||||
protected function escapeMdLabel(string $label): string
|
||||
{
|
||||
return str_replace(['[', ']', '!'], ['\\[', '\\]', '\\!'], $label);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user