routeFromRequestUri(); if (! $route) { return $queryVars; } $queryVars = [ 'kb_markdown_route' => $route['route'], ]; if (! empty($route['product'])) { $queryVars['kb_product_slug'] = $route['product']; } if (! empty($route['version'])) { $queryVars['kb_version_slug'] = $route['version']; } if (! empty($route['page'])) { $queryVars['kb_page_slug'] = $route['page']; } return $queryVars; } public function dispatch(): void { $route = get_query_var('kb_markdown_route'); $requestRoute = $this->routeFromRequestUri(); if (! $route && $requestRoute) { $route = $requestRoute['route']; } if (! $route) { $queryRoute = $this->routeFromQuery(); if (! $queryRoute) { return; } $requestRoute = $queryRoute; $route = $queryRoute['route']; } (new AccessController())->enforce(); get_header(); match ($route) { 'index' => $this->renderIndex(), 'product' => $this->renderProduct((string) ($requestRoute['product'] ?? get_query_var('kb_product_slug'))), 'version' => $this->renderVersion((string) ($requestRoute['product'] ?? get_query_var('kb_product_slug')), (string) ($requestRoute['version'] ?? get_query_var('kb_version_slug'))), 'page' => $this->renderPage((string) ($requestRoute['product'] ?? get_query_var('kb_product_slug')), (string) ($requestRoute['version'] ?? get_query_var('kb_version_slug')), trim((string) ($requestRoute['page'] ?? get_query_var('kb_page_slug')), '/')), default => $this->render404(), }; get_footer(); exit; } private function routeFromRequestUri(): array { $base = trim((string) Plugin::settings()['docs_base_slug'], '/') ?: 'docs'; $path = (string) wp_parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH); $path = trim(rawurldecode($path), '/'); if ($path === $base) { return ['route' => 'index']; } if (! str_starts_with($path . '/', $base . '/')) { return []; } $parts = array_values(array_filter(explode('/', substr($path, strlen($base))), static fn (string $part): bool => '' !== $part)); if (1 === count($parts)) { return ['route' => 'product', 'product' => sanitize_title($parts[0])]; } if (2 === count($parts)) { return ['route' => 'version', 'product' => sanitize_title($parts[0]), 'version' => sanitize_title($parts[1])]; } return [ 'route' => 'page', 'product' => sanitize_title($parts[0] ?? ''), 'version' => sanitize_title($parts[1] ?? ''), 'page' => sanitize_title(implode('/', array_slice($parts, 2))), ]; } private function routeFromQuery(): array { $route = sanitize_key(wp_unslash((string) ($_GET['kb_markdown_route'] ?? ''))); if (! $route) { return []; } return [ 'route' => $route, 'product' => sanitize_title(wp_unslash((string) ($_GET['kb_product_slug'] ?? ''))), 'version' => sanitize_title(wp_unslash((string) ($_GET['kb_version_slug'] ?? ''))), 'page' => sanitize_title(wp_unslash((string) ($_GET['kb_page_slug'] ?? ''))), ]; } public static function shortcodeDocsIndex(): string { return (new TemplateLoader())->capture('documentation-index', [ 'products' => self::productsWithVersions(), 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, ]); } public static function shortcodeDocsApp(array $atts = []): string { if (! (new AccessController())->canView()) { return ''; } $atts = shortcode_atts([ 'product' => '', 'version' => '', 'page' => '', ], $atts, 'kb_docs'); $router = new self(); $baseUrl = get_permalink() ?: home_url(add_query_arg([], (string) ($_SERVER['REQUEST_URI'] ?? '/'))); UrlBuilder::beginEmbed($baseUrl); try { $route = sanitize_key(wp_unslash((string) ($_GET['kb_docs_route'] ?? ''))); $product = sanitize_title(wp_unslash((string) ($_GET['kb_docs_product'] ?? $atts['product']))); $version = sanitize_title(wp_unslash((string) ($_GET['kb_docs_version'] ?? $atts['version']))); $page = sanitize_title(wp_unslash((string) ($_GET['kb_docs_page'] ?? $atts['page']))); if (! $route) { $route = $product ? ($version ? ($page ? 'page' : 'version') : 'product') : 'index'; } return $router->captureRoute($route, $product, $version, $page); } finally { UrlBuilder::endEmbed(); } } public static function shortcodeProductIndex(array $atts): string { $atts = shortcode_atts(['product' => ''], $atts, 'kb_product_index'); $router = new self(); return $router->captureProduct((string) $atts['product']); } private function renderIndex(): void { echo $this->captureIndex(); } private function captureIndex(): string { return (new TemplateLoader())->capture('documentation-index', [ 'products' => self::productsWithVersions(), 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, ]); } private function renderProduct(string $productSlug): void { echo $this->captureProduct($productSlug); } private function captureProduct(string $productSlug): string { $product = get_term_by('slug', $productSlug, 'kb_product'); if (! $product) { return $this->capture404(); } $versions = $this->versionsForProduct($productSlug); return (new TemplateLoader())->capture('product', [ 'product' => $product, 'versions' => $versions, 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, ]); } private function renderVersion(string $productSlug, string $versionSlug): void { echo $this->captureVersion($productSlug, $versionSlug); } private function captureVersion(string $productSlug, string $versionSlug): string { $product = get_term_by('slug', $productSlug, 'kb_product'); $version = get_term_by('slug', $versionSlug, 'kb_version'); if (! $product || ! $version) { return $this->capture404(); } $landing = $this->landingPageForVersion($productSlug, $versionSlug); if ($landing) { return $this->captureDocPage($landing, $productSlug, $versionSlug); } return (new TemplateLoader())->capture('version', [ 'product' => $product, 'version' => $version, 'pages' => $this->pagesForVersion($productSlug, $versionSlug), 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, ]); } private function renderPage(string $productSlug, string $versionSlug, string $pageSlug): void { echo $this->capturePage($productSlug, $versionSlug, $pageSlug); } private function capturePage(string $productSlug, string $versionSlug, string $pageSlug): string { $post = (new PageRepository())->findFrontendPage($productSlug, $versionSlug, $pageSlug); if (! $post && '' === $pageSlug) { $post = (new PageRepository())->findFrontendPage($productSlug, $versionSlug, 'index'); } if (! $post) { return $this->capture404(); } return $this->captureDocPage($post, $productSlug, $versionSlug); } private function renderDocPage(\WP_Post $post, string $productSlug, string $versionSlug): void { echo $this->captureDocPage($post, $productSlug, $versionSlug); } private function captureDocPage(\WP_Post $post, string $productSlug, string $versionSlug): string { $product = get_term_by('slug', $productSlug, 'kb_product'); $version = get_term_by('slug', $versionSlug, 'kb_version'); $navTree = json_decode((string) get_post_meta($post->ID, '_kb_nav_tree', true), true); return (new TemplateLoader())->capture('page', [ 'post' => $post, 'product' => $product, 'version' => $version, 'versions' => $this->versionsForProduct($productSlug), 'nav_tree' => is_array($navTree) ? $navTree : [], 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'product_slug' => $productSlug, 'version_slug' => $versionSlug, 'url_builder' => UrlBuilder::class, ]); } private function captureRoute(string $route, string $productSlug = '', string $versionSlug = '', string $pageSlug = ''): string { return match ($route) { 'index' => $this->captureIndex(), 'product' => $productSlug ? $this->captureProduct($productSlug) : $this->captureIndex(), 'version' => ($productSlug && $versionSlug) ? $this->captureVersion($productSlug, $versionSlug) : $this->captureIndex(), 'page' => ($productSlug && $versionSlug) ? $this->capturePage($productSlug, $versionSlug, $pageSlug) : $this->captureIndex(), default => $this->captureIndex(), }; } private function render404(): void { status_header(404); (new TemplateLoader())->render('search', [ 'title' => __('Documentation page not found.', 'kb-markdown-importer'), 'results' => [], 'query' => '', ]); } private function capture404(): string { status_header(404); return (new TemplateLoader())->capture('search', [ 'title' => __('Documentation page not found.', 'kb-markdown-importer'), 'results' => [], 'query' => '', ]); } public static function productsWithVersions(): array { $products = get_terms(['taxonomy' => 'kb_product', 'hide_empty' => false]); $items = []; if (is_wp_error($products)) { return []; } foreach ($products as $product) { $versions = (new self())->versionsForProduct($product->slug); if (! $versions) { continue; } $items[] = [ 'term' => $product, 'versions' => $versions, ]; } return $items; } private function versionsForProduct(string $productSlug): array { $query = new \WP_Query([ 'post_type' => 'kb_doc_page', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'tax_query' => [ ['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlug], ], ]); $versions = []; foreach ($query->posts as $postId) { foreach (wp_get_object_terms((int) $postId, 'kb_version') as $term) { $versions[$term->slug] = $term; } } uasort($versions, static fn ($a, $b): int => strnatcasecmp($b->name, $a->name)); return array_values($versions); } private function pagesForVersion(string $productSlug, string $versionSlug): array { $query = new \WP_Query([ 'post_type' => 'kb_doc_page', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_key' => '_kb_nav_order', 'orderby' => ['meta_value_num' => 'ASC', 'title' => 'ASC'], 'tax_query' => [ 'relation' => 'AND', ['taxonomy' => 'kb_product', 'field' => 'slug', 'terms' => $productSlug], ['taxonomy' => 'kb_version', 'field' => 'slug', 'terms' => $versionSlug], ], ]); return $query->posts; } private function landingPageForVersion(string $productSlug, string $versionSlug): ?\WP_Post { $repository = new PageRepository(); $landing = $repository->findFrontendPage($productSlug, $versionSlug, ''); if ($landing) { return $landing; } $pages = $this->pagesForVersion($productSlug, $versionSlug); return $pages[0] ?? null; } }