markdown document builder. * * Frontmatter mirrors the 42A mirror format (convex/mirroring/readability.ts) * adapted for first-party serving: * * --- * source_url: https://example.com/hello-world/ * title: "Hello: World" * generated_at: 2026-07-06T12:00:00Z * host: example.com * type: post * generator: ael-42a/1.0.0 * --- */ class Markdown_Converter { /** * Build (or fetch from cache) the full markdown document for a post. * * @param \WP_Post $post Post object. * @return string Markdown document. */ public function document( $post ) { $cached = Cache::get_markdown( $post ); if ( null !== $cached ) { return $cached; } $markdown = $this->build_document( $post ); Cache::set_markdown( $post, $markdown ); return $markdown; } /** * Build the document without touching the cache. * * @param \WP_Post $post Post object. * @return string */ public function build_document( $post ) { $permalink = get_permalink( $post ); $title = get_the_title( $post ); $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $frontmatter = array( '---', 'source_url: ' . $this->yaml_quote_if_needed( $permalink ), 'title: ' . $this->yaml_quote( $title ), 'generated_at: ' . gmdate( 'Y-m-d\TH:i:s\Z' ), 'host: ' . $host, 'type: ' . $post->post_type, 'generator: ael-42a/' . AEL42A_VERSION, '---', ); $body = $this->body( $post ); $document = implode( "\n", $frontmatter ) . "\n\n# " . $title . "\n\n" . $body; if ( 'product' === $post->post_type && function_exists( 'wc_get_product' ) ) { $details = $this->product_details( $post ); if ( '' !== $details ) { $document = rtrim( $document ) . "\n\n" . $details; } } return rtrim( $document ) . "\n"; } /** * Convert the rendered post content to markdown. * * @param \WP_Post $post Post object. * @return string */ public function body( $post ) { $html = apply_filters( 'the_content', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying the WordPress core content filter, not defining a plugin hook. $html = (string) $html; // Strauss-prefixed vendor build takes precedence when available. $league_class = '\\AEL42A\\Vendor\\League\\HTMLToMarkdown\\HtmlConverter'; if ( class_exists( $league_class ) ) { try { $converter = new $league_class( array( 'strip_tags' => true, 'header_style' => 'atx', 'remove_nodes' => 'script style', ) ); return trim( $converter->convert( $html ) ) . "\n"; } catch ( \Throwable $e ) { // Fall through to the built-in converter. } } $fallback = new Html_To_Markdown_Fallback(); return $fallback->convert( $html ); } /** * Structured product details section for WooCommerce products. * * @param \WP_Post $post Product post. * @return string Markdown section, or '' when unavailable. */ private function product_details( $post ) { $product = wc_get_product( $post->ID ); if ( ! $product ) { return ''; } $categories = array(); $terms = get_the_terms( $post->ID, 'product_cat' ); if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $categories[] = $term->name; } } $rows = array(); $rows[] = '## ' . __( 'Product details', 'ael-42a' ); $rows[] = ''; $rows[] = '- ' . __( 'Price', 'ael-42a' ) . ': ' . ( '' !== (string) $product->get_price() ? (string) $product->get_price() : __( 'n/a', 'ael-42a' ) ); $rows[] = '- ' . __( 'Currency', 'ael-42a' ) . ': ' . get_woocommerce_currency(); if ( $product->get_sku() ) { $rows[] = '- ' . __( 'SKU', 'ael-42a' ) . ': ' . $product->get_sku(); } $rows[] = '- ' . __( 'Stock status', 'ael-42a' ) . ': ' . $product->get_stock_status(); if ( ! empty( $categories ) ) { $rows[] = '- ' . __( 'Categories', 'ael-42a' ) . ': ' . implode( ', ', $categories ); } return implode( "\n", $rows ); } /** * Always double-quote a YAML value (used for title). * * @param string $value Raw value. * @return string */ private function yaml_quote( $value ) { $escaped = str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), (string) $value ); $escaped = str_replace( array( "\r", "\n" ), ' ', $escaped ); return '"' . $escaped . '"'; } /** * Quote a YAML scalar only when it contains special characters. * * @param string $value Raw value. * @return string */ private function yaml_quote_if_needed( $value ) { $value = (string) $value; if ( preg_match( '/[:#\[\]{},&*?|<>=!%@`\'"\\\\]/', $value ) || '' === $value ) { return $this->yaml_quote( $value ); } return $value; } }