.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 ) );
}
}