addExtension(new CommonMarkCoreExtension); $environment->addExtension(new TableExtension); $this->converter = new MarkdownConverter($environment); } /** * @return array> */ public function sync(): array { $sourcePath = base_path(config('help.source_path')); if (! $this->files->exists($sourcePath)) { throw new RuntimeException('Help source directory not found: '.$sourcePath); } $articles = collect(); foreach (config('help.audiences', []) as $audience) { $audiencePath = $sourcePath.DIRECTORY_SEPARATOR.$audience; if (! $this->files->isDirectory($audiencePath)) { continue; } $files = $this->files->allFiles($audiencePath); /** @var SplFileInfo $file */ foreach ($files as $file) { if ($file->getExtension() !== 'md') { continue; } $parsed = $this->parseFile($file); $articles->push($parsed); } } $disk = config('help.disk'); $compiledPath = trim(config('help.compiled_path'), '/'); $written = []; foreach ($articles->groupBy(fn ($article) => $article['audience'].'::'.$article['locale']) as $key => $group) { [$audience, $locale] = explode('::', $key); $path = sprintf('%s/%s/%s/articles.json', $compiledPath, $audience, $locale); Storage::disk($disk)->put($path, $group->values()->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); Cache::forget($this->cacheKey($audience, $locale)); $written[$audience][$locale] = $group->count(); } return $written; } private function parseFile(SplFileInfo $file): array { $contents = $this->files->get($file->getPathname()); if (! Str::startsWith($contents, "---\n")) { throw new RuntimeException('Missing front matter in '.$file->getPathname()); } $pattern = '/^---\s*\n(?P.*?)-{3}\s*\n(?P.*)$/s'; if (! preg_match($pattern, $contents, $matches)) { throw new RuntimeException('Unable to parse front matter for '.$file->getPathname()); } $frontMatter = Yaml::parse(trim($matches['yaml'] ?? '')) ?? []; $frontMatter = array_map(static fn ($value) => $value ?? null, $frontMatter); $this->validateFrontMatter($frontMatter, $file->getPathname()); $body = trim($matches['body'] ?? ''); $html = trim($this->converter->convert($body)->getContent()); $updatedAt = now()->setTimestamp($file->getMTime())->toISOString(); return array_merge($frontMatter, [ 'body_markdown' => $body, 'body_html' => $html, 'source_path' => $file->getRelativePathname(), 'updated_at' => $updatedAt, ]); } private function validateFrontMatter(array $frontMatter, string $path): void { $required = config('help.required_front_matter', []); foreach ($required as $key) { if (! Arr::exists($frontMatter, $key)) { throw new RuntimeException(sprintf('Missing front matter key "%s" in %s', $key, $path)); } } $audiences = config('help.audiences', []); $audience = $frontMatter['audience']; if (! in_array($audience, $audiences, true)) { throw new RuntimeException(sprintf('Invalid audience "%s" in %s', $audience, $path)); } } private function cacheKey(string $audience, string $locale): string { return sprintf('help.%s.%s', $audience, $locale); } }