.md` - rewrite-based, best effort. * 3. `Accept: text/markdown` preferred over text/html - content negotiation. * * Every HTML singular view advertises the markdown variant via a * `` element and a `Link:` HTTP header. */ class Markdown_Controller { const QUERY_VAR = 'ael42a_md'; /** * Telemetry counters. * * @var Telemetry */ private $telemetry; /** * Converter instance. * * @var Markdown_Converter */ private $converter; /** * Constructor. * * @param Telemetry $telemetry Telemetry counters. */ public function __construct( $telemetry ) { $this->telemetry = $telemetry; $this->converter = new Markdown_Converter(); } /** * Hook everything up. */ public function register_hooks() { add_action( 'init', array( __CLASS__, 'add_rewrite_rules' ) ); add_filter( 'query_vars', array( $this, 'register_query_vars' ) ); add_action( 'template_redirect', array( $this, 'maybe_serve_markdown' ), 1 ); add_action( 'template_redirect', array( $this, 'send_alternate_link_header' ), 20 ); add_action( 'wp_head', array( $this, 'print_alternate_link' ) ); } /** * `.md` suffix rewrite rules. Static so activation can call it before * the component graph exists. */ public static function add_rewrite_rules() { // Posts and flat slugs (single path segment). add_rewrite_rule( '([^/]+)\.md$', 'index.php?name=$matches[1]&' . self::QUERY_VAR . '=1', 'top' ); // Hierarchical pages (nested path). add_rewrite_rule( '(.+?/[^/]+)\.md$', 'index.php?pagename=$matches[1]&' . self::QUERY_VAR . '=1', 'top' ); } /** * Register our query var. * * @param string[] $vars Public query vars. * @return string[] */ public function register_query_vars( $vars ) { $vars[] = self::QUERY_VAR; return $vars; } /** * Serve the markdown variant when requested, then exit. */ public function maybe_serve_markdown() { if ( ! is_singular() ) { return; } $post = get_queried_object(); if ( ! $post instanceof \WP_Post || ! $this->is_servable( $post ) ) { return; } if ( ! $this->wants_markdown() ) { return; } $markdown = $this->converter->document( $post ); $this->telemetry->increment( 'markdown_hits' ); $this->telemetry->record_bot_hit_from_request(); status_header( 200 ); header( 'Content-Type: text/markdown; charset=utf-8' ); header( 'Vary: Accept' ); header( 'X-42A-AEL: ' . AEL42A_VERSION ); echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput -- plain text/markdown body, not HTML. exit; } /** * Whether the current request asked for markdown. * * @return bool */ private function wants_markdown() { // 1. ?format=markdown. if ( isset( $_GET['format'] ) && 'markdown' === sanitize_key( wp_unslash( $_GET['format'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return true; } // 2. `.md` rewrite matched. if ( '1' === get_query_var( self::QUERY_VAR ) || 1 === get_query_var( self::QUERY_VAR ) ) { return true; } // 3. Accept header prefers text/markdown over text/html. $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : ''; if ( '' !== $accept && false !== stripos( $accept, 'text/markdown' ) ) { $md_q = $this->accept_quality( $accept, 'text/markdown' ); $html_q = max( $this->accept_quality( $accept, 'text/html' ), $this->accept_quality( $accept, 'application/xhtml+xml' ) ); if ( $md_q > 0 && $md_q >= $html_q ) { return true; } } return false; } /** * Extract the q-value for a media type from an Accept header. * * @param string $accept Accept header. * @param string $type Media type to look for. * @return float Quality (0 when absent). */ private function accept_quality( $accept, $type ) { foreach ( explode( ',', $accept ) as $part ) { $pieces = array_map( 'trim', explode( ';', $part ) ); $media = strtolower( array_shift( $pieces ) ); if ( $media !== strtolower( $type ) ) { continue; } $q = 1.0; foreach ( $pieces as $param ) { if ( 0 === stripos( $param, 'q=' ) ) { $q = (float) substr( $param, 2 ); } } return $q; } return 0.0; } /** * Whether a post may be served as markdown. * * @param \WP_Post $post Post. * @return bool */ private function is_servable( $post ) { if ( 'publish' !== $post->post_status ) { return false; } if ( post_password_required( $post ) ) { return false; } if ( ! is_post_publicly_viewable( $post ) ) { return false; } return true; } /** * `Link:` header advertising the markdown alternate on HTML responses. */ public function send_alternate_link_header() { if ( headers_sent() || ! is_singular() ) { return; } $post = get_queried_object(); if ( ! $post instanceof \WP_Post || ! $this->is_servable( $post ) ) { return; } $url = self::markdown_url( $post ); header( 'Link: <' . esc_url_raw( $url ) . '>; rel="alternate"; type="text/markdown"', false ); } /** * `` element on singular HTML views. */ public function print_alternate_link() { if ( ! is_singular() ) { return; } $post = get_queried_object(); if ( ! $post instanceof \WP_Post || ! $this->is_servable( $post ) ) { return; } printf( '' . "\n", esc_url( self::markdown_url( $post ) ) ); } /** * Canonical markdown URL for a post. * * @param \WP_Post|int $post Post or ID. * @return string */ public static function markdown_url( $post ) { return add_query_arg( 'format', 'markdown', get_permalink( $post ) ); } }