diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md new file mode 100644 index 00000000..15760858 --- /dev/null +++ b/.gemini/GEMINI.md @@ -0,0 +1,42 @@ +# Copilot AI Coding Assistant Instructions for FlightPHP Docs + +Welcome to the FlightPHP Documentation Site! This project is the official documentation hub for the Flight PHP Framework. Please follow these guidelines when using AI coding assistants (like GitHub Copilot) to contribute to this repository. + +## Project Structure + +- **app/**: Contains the application logic for fetching, rendering, and displaying documentation content. This is a standard Flight PHP app, with customizations for documentation needs. +- **content/**: Holds all documentation content, organized by framework version (e.g., `v3/`). + - **content/v3/en/**: The *only* directory for direct documentation edits. All other language folders are generated via translation scripts and should not be manually edited. + - **content/v3/{lang}/**: Translated content for other languages. These are auto-generated and overwritten by scripts. +- **translate_content.php**: Script for translating English docs to other languages using an LLM API. Do not edit non-English docs directly. +- **public/**: Contains static assets and the main entry point (`index.php`). + +## Contribution Guidelines + +- **Add or update documentation only in `content/v3/en/`**. All other language folders are generated and will be overwritten. +- **When adding documentation, include practical, beginner-friendly examples**. Focus on helping new PHP developers understand the framework, but keep content useful for mid-level and senior devs too. +- **The tone should be casual, friendly, and fun**—like a good friend helping you learn. Avoid being overly silly or unprofessional. +- **This site is not a PHP tutorial**. Focus on Flight PHP concepts: framework basics, usage, scaling, best practices, and advanced features. +- **Do not edit translated files directly**. Use the translation script to update non-English docs. +- **Keep code and documentation clear and concise**. Favor real-world, copy-paste-ready examples. +- **If you add new features to the app logic, document them for future contributors.** +- **Check for existing examples before adding new ones** to avoid duplication. + +## AI Assistant Usage + +- When using AI assistants: + - Ensure all changes to documentation are made in English under `content/v3/en/`. + - For code changes, follow the existing structure and conventions in `app/`. + - Suggest practical, beginner-friendly examples for new docs. + - Maintain the project's friendly, approachable tone. + - Do not generate or suggest edits to translated files. + - If unsure about a translation or content structure, check the translation script or ask a maintainer. + +## Other Notes + +- The project uses standard Flight PHP conventions for routing and rendering. +- Static assets (CSS, JS, images) are in `public/`. +- The site is designed to be easy to run locally via php dev-server with `composer start` +- All contributions should be MIT licensed. + +Thank you for helping make FlightPHP docs awesome! diff --git a/app/commands/TranslateCommand.php b/app/commands/TranslateCommand.php new file mode 100644 index 00000000..f88508ec --- /dev/null +++ b/app/commands/TranslateCommand.php @@ -0,0 +1,303 @@ +option('--from-date', 'Skip files older than this date (YYYY-MM-DD)', null, 0) + ->option('--skip-files', 'Comma separated list of filenames to skip', null, '') + ->option('--dry-run', 'Run without making API calls or saving files', null, false) + ->option('--threads', 'Number of concurrent translation requests', null, 5); + } + + public function execute() + { + $io = $this->app()->io(); + $chatgpt_key = $this->config['chatgpt_key'] ?? ''; + + if (empty($chatgpt_key)) { + // fallback to env if not in config + $chatgpt_key = getenv('CHATGPT_KEY'); + } + + if (empty($chatgpt_key) && !$this->dryRun) { + $io->error('You need to set the chatgpt_key in config or CHATGPT_KEY environment variable to run this script', true); + return; + } + + $fromDate = $this->fromDate; + if ($fromDate) { + $fromDate = strtotime($fromDate . ' 00:00:00'); + } else { + $fromDate = strtotime($this->config['last_translation_run'] . ' 00:00:00'); + } + + $dryRun = $this->dryRun; + + $io->info("Translating content from " . date('Y-m-d', $fromDate ?: 0), true); + if ($dryRun) { + $io->warn('** DRY RUN MODE ** - No files will be modified.', true); + } + + $skipFilesInput = $this->skipFiles ?? ''; + $filenames_to_skip = array_filter(array_map('trim', explode(',', $skipFilesInput))); + + $languages = [ + 'es', + 'fr', + 'lv', + 'pt', + 'de', + 'ru', + 'zh', + 'ja', + 'ko', + 'uk', + 'id' + ]; + + $projectRoot = dirname(__DIR__, 2); + $top_level_files = glob($projectRoot . '/content/v3/en/*.md'); + $files = array_merge($top_level_files, glob($projectRoot . '/content/v3/en/**/*.md')); + + // Build queue of translation jobs + $translationJobs = []; + foreach ($files as $file) { + if ($fromDate && filemtime($file) < $fromDate) { + $io->comment("Skipping " . basename($file) . " because it's older than the from-date", true); + continue; + } + + if (in_array(basename($file), $filenames_to_skip)) { + $io->comment("Skipping " . basename($file) . " because it's in the skip list", true); + continue; + } + + foreach ($languages as $languageAbbreviation) { + $translationJobs[] = [ + 'file' => $file, + 'language' => $languageAbbreviation, + 'translatedFilePath' => str_replace('/en/', '/' . $languageAbbreviation . '/', $file) + ]; + } + } + + if (empty($translationJobs)) { + $io->info('No files to translate.', true); + return; + } + + $io->info("Total translation jobs: " . count($translationJobs), true); + + if ($dryRun) { + foreach ($translationJobs as $job) { + $io->comment(" [DRY RUN] Would translate " . basename($job['file']) . " to {$job['language']}", true); + } + } + + // Process jobs in batches based on thread count + $maxThreads = (int) $this->threads; + $totalJobs = count($translationJobs); + $processedJobs = 0; + + while (!$dryRun && $processedJobs < $totalJobs) { + $batch = array_slice($translationJobs, $processedJobs, $maxThreads); + $this->processBatch($batch, $chatgpt_key, $io); + $processedJobs += count($batch); + $io->info("Progress: {$processedJobs}/{$totalJobs} translations completed", true); + } + + // Cleanup orphaned files + if (!$dryRun) { + $this->cleanupOrphanedFiles($files, $languages, $projectRoot, $io); + // update config.php to have the last time this was run + $this->app()->handle([PROJECT_ROOT . '/vendor/bin/runway', 'config:set', 'last_translation_run', date('Y-m-d')]); + } else { + $io->comment("[DRY RUN] Skipping orphaned file cleanup check.", true); + } + + } + + /** + * Process a batch of translation jobs concurrently using curl_multi + */ + protected function processBatch(array $batch, string $apiKey, $io) + { + $mh = curl_multi_init(); + $handles = []; + $jobData = []; + + // Initialize all curl handles for the batch + foreach ($batch as $index => $job) { + $fileContent = file_get_contents($job['file']); + $messages = [ + [ + "role" => "system", + "content" => "You are a gifted translator focusing on the tech space. Today you are translating documentation for a PHP Framework called Flight (so please never translate the word 'Flight' as it's the name of the framework). You are going to receive content that is a markdown file. When you receive the content you'll translate it from english to the two letter language code that is specified. When you generate a response, you are going to ONLY send back the translated markdown content, no other replies or 'here is your translated markdown' type statements back, only the translated markdown content in markdown format. If you get a follow up response, you need to continue to markdown translation from the very character you left off at and complete the translation until the full page is done. THIS NEXT ITEM IS VERY IMPORTANT! Make sure that when you are translating any code in the markdown file that you ONLY translate the comments of the code and not the classes/methods/variables/links/urls/etc. This next part is also incredibly important or it will break the entire page!!!! Please don't translate any URLs or you will break my app and I will lose my job if this is not done correctly!!!!" + ], + [ + "role" => "user", + "content" => "Translate the following text from English to the two letter language code of {$job['language']}:\n\n{$fileContent}" + ] + ]; + + $ch = $this->createCurlHandle($apiKey, $messages); + curl_multi_add_handle($mh, $ch); + + $handles[(int) $ch] = $ch; + $jobData[(int) $ch] = [ + 'job' => $job, + 'messages' => $messages, + 'full_response' => '', + 'needs_continuation' => false + ]; + } + + // Execute all handles + $active = null; + do { + $status = curl_multi_exec($mh, $active); + if ($active) { + curl_multi_select($mh); + } + } while ($active && $status == CURLM_OK); + + // Collect results and check for continuation needs + foreach ($handles as $handleId => $ch) { + $response = curl_multi_getcontent($ch); + $responseArr = json_decode($response, true); + + $content = $responseArr['choices'][0]['message']['content'] ?? ''; + + if (!empty($content)) { + $jobData[$handleId]['full_response'] .= $content; + $jobData[$handleId]['messages'][] = [ + 'role' => 'assistant', + 'content' => $content + ]; + + // Check if we need continuation (response was truncated) + if (($responseArr['usage']['completion_tokens'] ?? 0) === 4096) { + $jobData[$handleId]['needs_continuation'] = true; + } + } else { + $io->error("Empty response for " . basename($jobData[$handleId]['job']['file']) . " ({$jobData[$handleId]['job']['language']})", true); + } + + curl_multi_remove_handle($mh, $ch); + curl_close($ch); + } + + curl_multi_close($mh); + + // Handle continuation requests for jobs that need them + foreach ($jobData as $data) { + while ($data['needs_continuation']) { + $io->comment(" Continuing translation for " . basename($data['job']['file']) . " ({$data['job']['language']})...", true); + + $ch = $this->createCurlHandle($apiKey, $data['messages']); + $response = curl_exec($ch); + curl_close($ch); + + $responseArr = json_decode($response, true); + $content = $responseArr['choices'][0]['message']['content'] ?? ''; + + if (empty($content)) { + $io->error("Empty continuation response for " . basename($data['job']['file']), true); + break; + } + + $data['full_response'] .= $content; + $data['messages'][] = [ + 'role' => 'assistant', + 'content' => $content + ]; + + // Check if we still need continuation + $data['needs_continuation'] = ($responseArr['usage']['completion_tokens'] ?? 0) === 4096; + } + + // Save the translated content + if (!empty($data['full_response'])) { + $translatedFilePath = $data['job']['translatedFilePath']; + $directory = dirname($translatedFilePath); + + if (!is_dir($directory)) { + mkdir($directory, 0775, true); + } + + file_put_contents($translatedFilePath, $data['full_response']); + $io->green(" Translated: " . basename($data['job']['file']) . " -> {$data['job']['language']}", true); + } + } + } + + /** + * Create a configured curl handle for the API request + */ + protected function createCurlHandle(string $apiKey, array $messages) + { + $ch = curl_init('https://api.x.ai/v1/chat/completions'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + "model" => "grok-4-fast-non-reasoning", + "messages" => $messages + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $apiKey, + 'Content-Type: application/json' + ]); + + return $ch; + } + + protected function cleanupOrphanedFiles(array $files, array $languages, string $projectRoot, $io) + { + $enFiles = []; + foreach ($files as $file) { + $enFiles[] = ltrim(str_replace(realpath($projectRoot . '/content/v3/en/'), '', realpath($file)), '/\\'); + } + + foreach ($languages as $languageAbbreviation) { + $langDir = $projectRoot . "/content/v3/{$languageAbbreviation}/"; + if (!is_dir($langDir)) + continue; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($langDir, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $translatedFile) { + if ($translatedFile->getExtension() !== 'md') + continue; + + $relativePath = ltrim(str_replace(realpath($langDir), '', $translatedFile->getRealPath()), '/\\'); + + if (!in_array($relativePath, $enFiles)) { + $io->error("Deleting orphaned file: {$translatedFile->getRealPath()}", true); + unlink($translatedFile->getRealPath()); + + $dir = dirname($translatedFile->getRealPath()); + while ($dir !== $langDir && is_dir($dir) && count(glob("$dir/*")) === 0) { + rmdir($dir); + $dir = dirname($dir); + } + } + } + } + } +} diff --git a/app/config/services.php b/app/config/services.php index 1ac328ed..e50dec18 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -14,7 +14,7 @@ * @var CustomEngine $app */ - // This translates some common parts of the page, not the content +// This translates some common parts of the page, not the content $app->register('translator', Translator::class); // Templating Engine used to render the views @@ -28,7 +28,7 @@ ); $latte->addExtension($translatorExtension); - $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension); + $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension); }); // Cache for storing parsedown and other things @@ -40,6 +40,6 @@ $app->register('parsedown', Parsedown::class); // Register the APM -$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json'); +$ApmLogger = LoggerFactory::create($config['runway']); $Apm = new Apm($ApmLogger); -$Apm->bindEventsToFlightInstance($app); \ No newline at end of file +$Apm->bindEventsToFlightInstance($app); diff --git a/app/middleware/HeaderSecurityMiddleware.php b/app/middleware/HeaderSecurityMiddleware.php index 9730d1ce..3a3dcb42 100644 --- a/app/middleware/HeaderSecurityMiddleware.php +++ b/app/middleware/HeaderSecurityMiddleware.php @@ -18,7 +18,7 @@ public function before() { } Flight::response()->header('X-Frame-Options', 'SAMEORIGIN'); - Flight::response()->header("Content-Security-Policy", "default-src 'self'; script-src 'self' https://api.github.com https://cdn.jsdelivr.net https://buttons.github.io https://unpkg.com https://opengraph.b-cdn.net https://www.googletagmanager.com 'nonce-" . $nonce . "'; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; img-src 'self' https://dcbadge.limes.pink https://img.shields.io https://cdn.jsdelivr.net data: https://api.github.com https://raw.githubusercontent.com; connect-src 'self' https://api.github.com; frame-src https://www.youtube.com"); + Flight::response()->header("Content-Security-Policy", "default-src 'self'; script-src 'self' https://api.github.com https://cdn.jsdelivr.net https://buttons.github.io https://unpkg.com https://opengraph.b-cdn.net https://www.googletagmanager.com 'nonce-" . $nonce . "'; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; img-src 'self' https://dcbadge.limes.pink https://img.shields.io https://cdn.jsdelivr.net data: https://api.github.com https://raw.githubusercontent.com; connect-src 'self' https://api.github.com https://cdn.jsdelivr.net; frame-src https://www.youtube.com"); Flight::response()->header('X-XSS-Protection', '1; mode=block'); Flight::response()->header('X-Content-Type-Options', 'nosniff'); Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade'); diff --git a/app/utils/DocsLogic.php b/app/utils/DocsLogic.php index 271e2aef..d1b12744 100644 --- a/app/utils/DocsLogic.php +++ b/app/utils/DocsLogic.php @@ -9,10 +9,10 @@ class DocsLogic { /** @var string */ - private const DS = DIRECTORY_SEPARATOR; + private const DS = DIRECTORY_SEPARATOR; /** @var string Path to the base content directory */ - public const CONTENT_DIR = __DIR__ . self::DS . '..' . self::DS . '..' . self::DS . 'content' . self::DS; + public const CONTENT_DIR = __DIR__ . self::DS . '..' . self::DS . '..' . self::DS . 'content' . self::DS; const AVAILABLE_LANGUAGES = [ 'en', @@ -35,7 +35,6 @@ class DocsLogic { * @param CustomEngine $app Flight Engine */ public function __construct(protected $app) { - } /** @@ -46,33 +45,34 @@ public function __construct(protected $app) { public function getLearnSectionNames(): array { return [ 'Core Components' => [ - [ 'url' => '/learn/routing', 'title' => 'Routing' ], - [ 'url' => '/learn/middleware', 'title' => 'Middleware' ], - [ 'url' => '/learn/autoloading', 'title' => 'Autoloading' ], - [ 'url' => '/learn/requests', 'title' => 'Requests' ], - [ 'url' => '/learn/responses', 'title' => 'Responses' ], - [ 'url' => '/learn/templates', 'title' => 'HTML Templates' ], - [ 'url' => '/learn/security', 'title' => 'Security' ], - [ 'url' => '/learn/configuration', 'title' => 'Configuration' ], - [ 'url' => '/learn/events', 'title' => 'Event Manager' ], - [ 'url' => '/learn/extending', 'title' => 'Extending Flight' ], - [ 'url' => '/learn/filtering', 'title' => 'Method Hooks and Filtering' ], - [ 'url' => '/learn/dependency-injection-container', 'title' => 'Dependency Injection' ], + ['url' => '/learn/routing', 'title' => 'Routing'], + ['url' => '/learn/middleware', 'title' => 'Middleware'], + ['url' => '/learn/autoloading', 'title' => 'Autoloading'], + ['url' => '/learn/requests', 'title' => 'Requests'], + ['url' => '/learn/responses', 'title' => 'Responses'], + ['url' => '/learn/templates', 'title' => 'HTML Templates'], + ['url' => '/learn/security', 'title' => 'Security'], + ['url' => '/learn/configuration', 'title' => 'Configuration'], + ['url' => '/learn/events', 'title' => 'Event Manager'], + ['url' => '/learn/extending', 'title' => 'Extending Flight'], + ['url' => '/learn/filtering', 'title' => 'Method Hooks and Filtering'], + ['url' => '/learn/dependency-injection-container', 'title' => 'Dependency Injection'], ], 'Utility Classes' => [ - [ 'url' => '/learn/collections', 'title' => 'Collections' ], - [ 'url' => '/learn/json', 'title' => 'JSON Wrapper' ], - [ 'url' => '/learn/pdo-wrapper', 'title' => 'PDO Wrapper' ], - [ 'url' => '/learn/uploaded-file', 'title' => 'Uploaded File Handler' ], + ['url' => '/learn/collections', 'title' => 'Collections'], + ['url' => '/learn/json', 'title' => 'JSON Wrapper'], + ['url' => '/learn/pdo-wrapper', 'title' => 'PdoWrapper'], + ['url' => '/learn/simple-pdo', 'title' => 'SimplePdo'], + ['url' => '/learn/uploaded-file', 'title' => 'Uploaded File Handler'], ], 'Important Concepts' => [ - [ 'url' => '/learn/why-frameworks', 'title' => 'Why a Framework?' ], - [ 'url' => '/learn/flight-vs-another-framework', 'title' => 'Flight vs Others' ], + ['url' => '/learn/why-frameworks', 'title' => 'Why a Framework?'], + ['url' => '/learn/flight-vs-another-framework', 'title' => 'Flight vs Others'], ], 'Other Topics' => [ - [ 'url' => '/learn/unit-testing', 'title' => 'Unit Testing' ], - [ 'url' => '/learn/ai', 'title' => 'AI & Developer Experience' ], - [ 'url' => '/learn/migrating-to-v3', 'title' => 'Migrating v2 -> v3' ], + ['url' => '/learn/unit-testing', 'title' => 'Unit Testing'], + ['url' => '/learn/ai', 'title' => 'AI & Developer Experience'], + ['url' => '/learn/migrating-to-v3', 'title' => 'Migrating v2 -> v3'], ] ]; } @@ -83,25 +83,31 @@ public function getLearnSectionNames(): array { * @param string $latte_file The path to the Latte template file to be rendered. * @param array $params An optional array of parameters to be passed to the template. */ - public function renderPage(string $latte_file, array $params = []) { - $request = $this->app->request(); - $uri = $request->url; + public function renderPage(string $latte_file, array $params = []) { + $request = $this->app->request(); + $uri = $request->url; - if (str_contains($uri, '?')) { - $uri = substr($uri, 0, strpos($uri, '?')); - } - - - // Here we can set variables that will be available on any page - $params['url'] = $request->getScheme() . '://' . $request->getHeader('Host') . $uri; - $params['nonce'] = HeaderSecurityMiddleware::$nonce; - $params['q'] = $request->query['q'] ?? ''; + if (str_contains($uri, '?')) { + $uri = substr($uri, 0, strpos($uri, '?')); + } $startTime = microtime(true); - $this->app->latte()->render($latte_file, $params); + if (!empty($params['raw_markdown']) && (str_contains($request->header('Accept'), 'text/plain') || str_contains($request->header('Accept'), 'text/markdown'))) { + $this->app->response()->header('Content-Type', 'text/markdown; charset=utf-8'); + $this->app->response()->write($params['raw_markdown']); + } else { + + // Here we can set variables that will be available on any page + $params['url'] = $request->getScheme() . '://' . $request->getHeader('Host') . $uri; + $params['nonce'] = HeaderSecurityMiddleware::$nonce; + $params['q'] = $request->query['q'] ?? ''; + + $this->app->latte()->render($latte_file, $params); + } + $executionTime = microtime(true) - $startTime; - $this->app->eventDispatcher()->trigger('flight.view.rendered', $latte_file.':'.$uri, $executionTime); - } + $this->app->eventDispatcher()->trigger('flight.view.rendered', $latte_file . ':' . $uri, $executionTime); + } /** * Sets up the translator service with the specified language and version. @@ -114,7 +120,7 @@ public function setupTranslatorService(string $language, string $version): Trans $Translator = $this->app->translator(); $Translator->setLanguage($language); $Translator->setVersion($version); - return $Translator; + return $Translator; } /** @@ -126,8 +132,8 @@ public function setupTranslatorService(string $language, string $version): Trans * * @return void */ - public function compileSinglePage(string $language, string $version, string $section) { - $app = $this->app; + public function compileSinglePage(string $language, string $version, string $section) { + $app = $this->app; // Check if the language is valid if ($this->checkValidLanguage($language) === false) { @@ -145,24 +151,26 @@ public function compileSinglePage(string $language, string $version, string $sec $cacheHit = true; $cacheKey = $section . '_html_' . $language . '_' . $version; $markdown_html = $app->cache()->retrieve($cacheKey); + $rawMarkdown = $Translator->getMarkdownLanguageFile($section . '.md'); if ($markdown_html === null) { $cacheHit = false; - $markdown_html = $app->parsedown()->text($Translator->getMarkdownLanguageFile($section . '.md')); + $markdown_html = $app->parsedown()->text($rawMarkdown); $markdown_html = Text::addClassesToElements($markdown_html); $app->cache()->store($cacheKey, $markdown_html, 86400); // 1 day } - $app->eventDispatcher()->trigger('flight.cache.checked', 'compile_single_page_'.$cacheKey, $cacheHit, microtime(true) - $cacheStartTime); + $app->eventDispatcher()->trigger('flight.cache.checked', 'compile_single_page_' . $cacheKey, $cacheHit, microtime(true) - $cacheStartTime); - $markdown_html = $this->wrapContentInDiv($markdown_html); + $markdown_html = $this->wrapContentInDiv($markdown_html); - $this->renderPage('single_page.latte', [ - 'page_title' => $section, - 'markdown' => $markdown_html, + $this->renderPage('single_page.latte', [ + 'page_title' => $section, + 'markdown' => $markdown_html, 'version' => $version, 'language' => $language, - ]); - } + 'raw_markdown' => $rawMarkdown, + ]); + } /** * Compiles the Scrollspy page based on the provided language, version, section, and sub-section. @@ -172,7 +180,7 @@ public function compileSinglePage(string $language, string $version, string $sec * @param string $section The main section of the documentation. * @param string $sub_section The sub-section of the documentation. */ - public function compileScrollspyPage(string $language, string $version, string $section, string $sub_section) { + public function compileScrollspyPage(string $language, string $version, string $section, string $sub_section) { $app = $this->app; // Check if the language is valid @@ -195,18 +203,19 @@ public function compileScrollspyPage(string $language, string $version, string $ $cacheHit = true; $cacheKey = $sub_section_underscored . '_html_' . $language . '_' . $version; $markdown_html = $app->cache()->retrieve($cacheKey); + $rawMarkdown = $Translator->getMarkdownLanguageFile('/' . $section_file_path . '/' . $sub_section_underscored . '.md'); if ($markdown_html === null) { $cacheHit = false; - $markdown_html = $app->parsedown()->text($Translator->getMarkdownLanguageFile('/' . $section_file_path . '/' . $sub_section_underscored . '.md')); + $markdown_html = $app->parsedown()->text($rawMarkdown); $heading_data = []; - $markdown_html = Text::generateAndConvertHeaderListFromHtml($markdown_html, $heading_data, $section_file_path.'/'.$sub_section); + $markdown_html = Text::generateAndConvertHeaderListFromHtml($markdown_html, $heading_data, $section_file_path . '/' . $sub_section); $markdown_html = Text::addClassesToElements($markdown_html); $app->cache()->store($sub_section_underscored . '_heading_data_' . $language . '_' . $version, $heading_data, 86400); // 1 day $app->cache()->store($cacheKey, $markdown_html, 86400); // 1 day } - $app->eventDispatcher()->trigger('flight.cache.checked', 'compile_scrollspy_page_'.$cacheKey, $cacheHit, microtime(true) - $cacheStartTime); + $app->eventDispatcher()->trigger('flight.cache.checked', 'compile_scrollspy_page_' . $cacheKey, $cacheHit, microtime(true) - $cacheStartTime); // pull the title out of the first h1 tag $page_title = ''; @@ -223,9 +232,10 @@ public function compileScrollspyPage(string $language, string $version, string $ $params = [ 'custom_page_title' => ($page_title ? $page_title . ' - ' : '') . $Translator->translate($section), + 'raw_markdown' => $rawMarkdown, 'markdown' => $markdown_html, 'heading_data' => $heading_data, - 'relative_uri' => '/'.$section_file_path, + 'relative_uri' => '/' . $section_file_path, 'version' => $version, 'language' => $language, ]; @@ -237,22 +247,22 @@ public function compileScrollspyPage(string $language, string $version, string $ } $this->renderPage('single_page_scrollspy.latte', $params); - } + } /** - * This is necessary to encapsulate contents (

,

, 
    ,