Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/content-parser-interface
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add extensible content parser support and a JSON preview endpoint for AT Protocol records.
40 changes: 40 additions & 0 deletions includes/class-atmosphere.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public function init(): void {
// Plugin integrations.
Load::init();

// JSON preview for AT Protocol records.
\add_action( 'template_redirect', array( $this, 'preview' ) );

// Post lifecycle hooks.
\add_action( 'transition_post_status', array( $this, 'on_status_change' ), 10, 3 );

Expand Down Expand Up @@ -145,6 +148,43 @@ public function serve_wellknown_publication(): void {
exit;
}

/**
* Serve a JSON preview of the AT Protocol record for a post.
*
* Append ?atproto to a singular post URL to see the document
* record JSON. Requires the edit_posts capability.
*/
public function preview(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['atproto'] ) || ! \is_singular() ) {
return;
}

if ( ! \current_user_can( 'edit_posts' ) ) {
return;
}

$post = \get_queried_object();

if ( ! $post instanceof \WP_Post ) {
return;
}

if ( ! \in_array( $post->post_type, Backfill::syncable_post_types(), true ) ) {
\status_header( 404 );
exit;
}

$transformer = new Document( $post );
$record = $transformer->transform();

\status_header( 200 );
\header( 'Content-Type: application/json; charset=utf-8' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
echo \wp_json_encode( $record, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
exit;
}

/**
* Handle post status transitions.
*
Expand Down
42 changes: 42 additions & 0 deletions includes/content-parser/interface-content-parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/**
* Content parser interface for AT Protocol content formats.
*
* Plugins can implement this interface to provide custom content
* parsers for the site.standard.document content union field.
*
* @package Atmosphere
*/

namespace Atmosphere\Content_Parser;

\defined( 'ABSPATH' ) || exit;

/**
* Content parser contract.
*/
interface Content_Parser {

/**
* Parse WordPress post content into an AT Protocol content object.
*
* The returned array must include a '$type' key identifying the
* lexicon type (e.g. 'at.markpub.markdown').
*
* Receives raw post content so parsers can choose their own
* strategy: parse_blocks() for block-aware parsing, or
* apply_filters( 'the_content', ... ) for rendered HTML.
*
* @param string $content Raw post content (post_content).
* @param \WP_Post $post The WordPress post object.
* @return array AT Protocol content object.
*/
public function parse( string $content, \WP_Post $post ): array;

/**
* The lexicon NSID this parser produces.
*
* @return string e.g. 'at.markpub.markdown'.
*/
public function get_type(): string;
}
49 changes: 48 additions & 1 deletion includes/transformer/class-document.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

\defined( 'ABSPATH' ) || exit;

use Atmosphere\Content_Parser\Content_Parser;
use function Atmosphere\build_at_uri;
use function Atmosphere\get_did;
use function Atmosphere\sanitize_text;
Expand Down Expand Up @@ -55,10 +56,13 @@ public function transform(): array {
'publishedAt' => $this->to_iso8601( $this->object->post_date_gmt ),
);

// Publication reference.
// Publication reference (required by spec).
$pub_tid = \get_option( 'atmosphere_publication_tid' );
if ( $pub_tid ) {
$record['site'] = build_at_uri( get_did(), 'site.standard.publication', $pub_tid );
} else {
// Fall back to site URL for standalone documents.
$record['site'] = \untrailingslashit( \get_home_url() );
}

// Relative path.
Expand Down Expand Up @@ -89,6 +93,12 @@ public function transform(): array {
$record['textContent'] = $text_content;
}

// Parsed rich content (open union).
$content = $this->get_content();
if ( ! empty( $content ) ) {
$record['content'] = $content;
}

// Tags.
$tags = $this->collect_tags( $this->object );
if ( ! empty( $tags ) ) {
Expand Down Expand Up @@ -140,6 +150,43 @@ public function get_rkey(): string {
return $rkey;
}

/**
* Get parsed content for the document's content union field.
*
* @return array|null Parsed content object or null.
*/
private function get_content(): ?array {
if ( empty( \trim( $this->object->post_content ) ) ) {
return null;
}

/**
* Filters the content parser used for site.standard.document records.
*
* Return a Content_Parser instance to provide a parser.
* Return null to disable the content field entirely.
*
* @param Content_Parser|null $parser The content parser. Default: null.
* @param \WP_Post $post The WordPress post.
*/
$parser = \apply_filters( 'atmosphere_content_parser', null, $this->object );

if ( ! $parser instanceof Content_Parser ) {
return null;
}

$content = $parser->parse( $this->object->post_content, $this->object );

/**
* Filters the parsed content object before adding to the document record.
*
* @param array $content The parsed content object.
* @param \WP_Post $post The WordPress post.
* @param Content_Parser $parser The parser that produced the content.
*/
return \apply_filters( 'atmosphere_document_content', $content, $this->object, $parser );
}

/**
* Render post content to plain text.
*
Expand Down
36 changes: 36 additions & 0 deletions tests/phpunit/tests/transformer/class-stub-parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Stub content parser for testing.
*
* @package Atmosphere
*/

namespace Atmosphere\Tests\Transformer;

use Atmosphere\Content_Parser\Content_Parser;

/**
* Stub content parser that returns raw content as-is.
*/
class Stub_Parser implements Content_Parser {

/**
* {@inheritDoc}
*/
public function get_type(): string {
return 'test.stub.parser';
}

/**
* {@inheritDoc}
*
* @param string $content Raw post content.
* @param \WP_Post $post The WordPress post object.
*/
public function parse( string $content, \WP_Post $post ): array { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return array(
'$type' => 'test.stub.parser',
'text' => $content,
);
}
}
Loading