path = $this->normalizeBundlePath($path); $this->dataDir = $this->path . 'data/'; $this->registerDefaultManagers(); } /** * Return current page number */ public function getCurrentPage(): int { return $this->currentPage; } /** * Return bundle's path */ public function getPath(): string { return $this->path; } /** * Return bundle's data dir */ public function getDataDir(): string { return $this->dataDir; } /** * Return bundle's filesystem instance */ public function getDisk(): FilesystemAdapter { return $this->disk; } /** * Return the section where this bundle is located */ public function getSection(): ?Bundle { if (!isset($this->section)) { $parts = preg_split('#/#', $this->path, -1, PREG_SPLIT_NO_EMPTY); if (count($parts) > 0) { $this->section = new Bundle($parts[0], $this->disk); } else { $this->section = null; } } return $this->section; } /** * Return the parent bundle where this bundle is located */ public function getParent(): ?Bundle { if (!isset($this->parent)) { $parentPath = dirname($this->getPath()); if ($parentPath === $this->getPath()) { $this->parent = null; } else { $this->parent = new Bundle($parentPath, $this->getDisk()); } } return $this->parent; } /** * Return an array containing direct children of this bundle */ public function getDirectChildren(): array { if (!isset($this->directChildren)) { $this->directChildren = static::findBundles($this->getDisk(), $this->getPath()); } return $this->directChildren; } public function getArticleTitle(): string { return $this->metadata()->get('title') ?? Str::title(basename($this->path)); } public function getSiteTitle(): string { $title = $this->getArticleTitle(); $section = $this->getSection(); if (!empty($section) && $section->getPath() !== $this->getPath()) { $title .= ' - ' . $section->getArticleTitle(); } return $title; } /** * Return "virtual" metadata, which is a Collection of extended metadata, * with additions and corrections from the "metadata.json" file */ public function virtualMetadata(): Collection { $mergedData = []; foreach ($this->metadata()->get('virtualMetadata', []) as $additionalFile) { $mergedData = array_merge_recursive($mergedData, $this->metadata($additionalFile)->all() ?? []); } $replacedData = array_merge_recursive($mergedData, $this->metadata('metadata')->all() ?? []); return collect($replacedData); } /** * Return a boolean value indicating if there already is a bundle in * specified path */ public function exists(): bool { return $this->markdown()->exists(); } /** * Load everything */ public function load(): self { $this->loadAttachments(); $this->loadMetadata(); $this->loadMarkdown(); return $this; } /** * Store all files of the bundle */ public function save(): bool { $attachmentsSaved = $this->saveAttachments(); $metadataSaved = $this->saveMetadata(); $markdownSaved = $this->saveMarkdown(); if ( $attachmentsSaved || $metadataSaved || $markdownSaved ) { $this->touch(); return true; } return false; } /** * Repair bundle */ public function repair(): bool { $this->load(); $this->repairCover(); $this->lintMarkdown(); $this->repairAttachments(); return $this->save(); } /** * "Touches" a bundle, forgetting some cached data. */ public function touch(bool $ignoreFeed = false) { if (!$ignoreFeed) { Cache::forget('feed'); } $cacheKey = sprintf('render_%s', Str::slug($this->getPath())); Cache::forget($cacheKey); $parent = $this->getParent(); if (!empty($parent)) { // Always ignore feed when touching parent bundles $parent->touch(true); } //TODO: Touch bundles linking this one } public function render(bool $ignoreCache = false) { $renderer = BundleRenderer::getBundleRendererFor($this); $result = $renderer->render($ignoreCache); return $result; } public function renderCard() { $renderer = BundleRenderer::getBundleRendererFor($this); return $renderer->renderCard(); } /** * Return a list of bundles in the specified path */ public static function findBundles(FilesystemAdapter $disk, ?string $path = '/', bool $recursive = false, bool $sortByDate = true): array { $bundles = []; if ($recursive) { $bundles = $disk ->listContents($path, $recursive) ->filter(fn (StorageAttributes $attributes) => ($attributes->isFile() && Str::endsWith($attributes->path(), '.md'))) ->map(fn (StorageAttributes $attributes) => new Bundle(dirname($attributes->path()), $disk)) ->toArray(); } else { $directories = $disk->directories($path); foreach ($directories as $directory) { $bundle = new Bundle($directory, $disk); if ($bundle->exists()) { $bundles[] = $bundle; } } } if ($sortByDate) { $bundles = collect($bundles) ->sortBy([ fn (Bundle $a, Bundle $b) => Carbon::parse($a->metadata()->get('date')) ->gt(Carbon::parse($b->metadata()->get('date'))), fn (Bundle $a, Bundle $b) => Carbon::parse($b->metadata()->get('date')) ->gt(Carbon::parse($a->metadata()->get('date'))), ]) ->toArray(); } return $bundles; } public static function getFeedItems(FilesystemAdapter $disk) { $subBundles = Bundle::findBundles($disk, '/', true); $subBundles = collect($subBundles) ->filter(fn (Bundle $bundle) => !empty($bundle->metadata()->get('date'))) ->sort(function (Bundle $bundleA, Bundle $bundleB) { return Carbon::parse($bundleA->metadata()->get('date'))->lt(Carbon::parse($bundleB->metadata()->get('date'))); }) ->map(fn (Bundle $bundle) => $bundle->load()) ->take(10); return $subBundles; } public static function renderFeed(FilesystemAdapter $disk) { return Cache::rememberForever('feed', function () use ($disk) { $lastBundles = static::getFeedItems($disk); return [ '/index.xml' => (string) view('feed', [ 'bundles' => $lastBundles, 'lastBuildDate' => now()->toRssString(), ]), ]; }); } private function repairCover() { $cover = $this->metadata()->get('cover'); if (empty($cover)) { return; } if (is_array($cover)) { $path = $cover['url']; foreach ($this->attachments(AttachmentsManager::Images)->manager()->get('files') as $ref => $data) { if (Str::endsWith($data['filename'], $path)) { $this->metadata()->set('cover', $ref); } } } } /** * Return a normalized representation of bundle's path */ private function normalizeBundlePath(string $path): string { if ($path === './' || $path === '' || $path === '.') { return '/'; } $parts = preg_split('#/#', $path, -1, PREG_SPLIT_NO_EMPTY); $count = count($parts); if ($count >= 2 && $parts[$count - 2] === 'page') { // Requested URL is pagination $page = array_pop($parts); if (!is_numeric($page) || $page < 1) { throw new Exception(sprintf('Invalid page number: %s', $page)); } $this->currentPage = $page; // Pop "page" out of the parts array_pop($parts); } return Str::start(Str::finish(implode('/', $parts), '/'), '/'); } /** * Register default managers */ private function registerDefaultManagers(): void { $this->markdown(); $this->metadata(); $this->metadata('metadata'); $this->attachments(AttachmentsManager::Images); $this->attachments(AttachmentsManager::Sounds); $this->attachments(AttachmentsManager::Videos); } /** * Get a complete filename prefixed with bundle's path */ private function getFilenameInBundle(string $filename, ?string $extension = null): string { return $this->getFullpath($this->path, $filename, $extension); } /** * Get a complete filename prefixed with bundle's data dir */ private function getFilenameInDataBundle(string $filename, ?string $extension = null): string { return $this->getFullpath($this->dataDir, $filename, $extension); } /** * Return full path of specified filename in specified root directory, and * optionally add specified extension */ private function getFullpath(string $root, string $filename, ?string $extension = null): string { $filename = Str::remove($root, $filename); if (!empty($extension) && !Str::endsWith($filename, $extension)) { $filename .= $extension; } return sprintf('%s%s', $root, $filename); } }