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 build/blogroll/block.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build/blogroll/index.asset.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions build/blogroll/index.js

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions build/blogroll/render.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion docs/developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ block has no link blocks, so such a post renders without being opened; the edito
migrates it to link blocks when it is opened (a block deprecation, see
`src/blogroll/deprecated.js`).

A list whose `listed` attribute is `false` keeps its own file, `?opml&group=<anchor>`,
and its own `rel="blogroll"` link on its page, but stays out of the file of the page:
`Opml::listed_groups()` is what the page file, the page's own `rel="blogroll"` link and
the index are built from (`Index::has_listed_blogroll()`), while `Opml::all_groups()`
keeps every list, listed or not. Each group carries an `own_file` flag, and
`Opml::group_url()` turns it into an address, so the links in the head and the download
under a list never work the rule out again. A page whose lists are all unlisted has no
file of its own, is not advertised as a whole and is not listed in the directory.

Every entry is marked up as an [h-card](https://microformats.org/wiki/h-card) with
[XFN](https://gmpg.org/xfn/) relationships on the link, in an
[XOXO](https://microformats.org/wiki/xoxo) list:
Expand Down Expand Up @@ -61,7 +70,7 @@ every other anchor on the page, appending `-1`, `-2` if needed. An anchor set by
under Advanced is left alone. Pages saved before anchors existed get theirs on the
fly: `Anchors::add()` is a pure transform that adds the missing anchors to the blogroll
block comments only, with the same rules. It runs on `the_content` before `do_blocks()`
and inside `Opml::extract_groups()`, so the ids and the group links are there from the
and inside `Opml::all_groups()`, so the ids and the group links are there from the
first view on. On that first singular view `Anchors::migrate()` also writes the result
into `post_content`, directly, so there is no revision, no modified date and no save
hook for a change nobody made.
Expand Down
33 changes: 29 additions & 4 deletions includes/class-index.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<?php
/**
* Private taxonomy that indexes posts containing a blogroll block.
* Private taxonomy that indexes the posts with a blogroll to subscribe to.
*
* @package Blockroll
*/

namespace Blockroll;

/**
* Keep track of which posts contain a blogroll block.
* Keep track of which posts offer a blogroll, so the directory and the
* front page can point at them without reading every post.
*
* The taxonomy is an index only; the link data lives in the block attributes.
* A post is in the index when it has a list that is part of the file of
* its page; a page of unlisted lists has nothing to offer and stays out.
* The taxonomy is an index only; the link data lives in the block
* attributes.
*/
class Index {
const TAXONOMY = 'blockroll_has';
Expand Down Expand Up @@ -45,12 +49,16 @@ public static function sync( $post_id, $post ) {
return;
}

\wp_set_object_terms( $post_id, self::has_blogroll( $post ) ? self::TERM : array(), self::TAXONOMY );
\wp_set_object_terms( $post_id, self::has_listed_blogroll( $post ) ? self::TERM : array(), self::TAXONOMY );
}

/**
* Whether a post contains a blogroll block.
*
* A cheap string search, no parsing: this runs on every singular
* request. Whether any of the lists is offered for subscription is a
* question for has_listed_blogroll().
*
* @param \WP_Post|int $post Post.
* @return bool True when it does.
*/
Expand All @@ -59,6 +67,23 @@ public static function has_blogroll( $post ) {
return $post && \has_block( 'blockroll/blogroll', $post );
}

/**
* Whether a post has a file of its own, a listed list.
*
* An unlisted list keeps out of the file of its page and can still be
* subscribed to on its own, so this is not the same question as
* whether the page has a blogroll at all. Parses the content, so it is
* asked on save only; what the directory lists is the index this
* writes.
*
* @param \WP_Post|int $post Post.
* @return bool True when at least one list is in the file of the page.
*/
public static function has_listed_blogroll( $post ) {
$post = \get_post( $post );
return self::has_blogroll( $post ) && (bool) Opml::listed_groups( $post );
}

/**
* The queried post of a singular request, when it has a blogroll.
*
Expand Down
139 changes: 109 additions & 30 deletions includes/class-opml.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,26 @@ public static function feed_blogroll() {
}

/**
* Collect normalized links from all blogroll blocks in a post.
* The links that make up the file of a page, in one flat list.
*
* An unlisted list is not part of it; all_groups() has every list.
*
* @param \WP_Post $post Post object.
* @return array Normalized links.
*/
public static function extract_links( $post ) {
public static function page_links( $post ) {
$links = array();
foreach ( self::extract_groups( $post ) as $group ) {
foreach ( self::listed_groups( $post ) as $group ) {
$links = \array_merge( $links, $group['links'] );
}
return $links;
}

/**
* Collect the blogroll blocks of a post, each with its own name and links.
* Every blogroll of a post, each with its own name, links and address.
*
* Listed or not: what goes into the file of the page is what
* listed_groups() returns.
*
* The name is the one WordPress keeps when a block is renamed in the
* editor, so a page with several blogrolls can say what each one is
Expand All @@ -170,7 +175,7 @@ public static function extract_links( $post ) {
* @param \WP_Post $post Post object.
* @return array List of arrays with a "name", an "anchor" and a "links" key.
*/
public static function extract_groups( $post ) {
public static function all_groups( $post ) {
static $cache = array();

if ( isset( $cache[ $post->ID ] ) && $cache[ $post->ID ]['content'] === $post->post_content ) {
Expand All @@ -187,6 +192,9 @@ public static function extract_groups( $post ) {
'name' => \sanitize_text_field( (string) ( $block['attrs']['metadata']['name'] ?? '' ) ),
'anchor' => \trim( (string) ( $block['attrs']['anchor'] ?? '' ) ),
'links' => $links,
// A list can be unlisted, kept out of the file of
// its page, and still have one of its own.
'listed' => false !== ( $block['attrs']['listed'] ?? true ),
);
}
}
Expand All @@ -197,6 +205,17 @@ public static function extract_groups( $post ) {
};
$walker( \parse_blocks( Anchors::add( $post->post_content ) ) );

// A list has a file of its own when it is unlisted, since it is then
// not in the file of the page, and when it is one of several listed
// ones. A single listed list is the file of the page and shares its
// address. Every caller asks the group instead of working it out
// again: the link in the head, the download under the list.
$listed = \count( \wp_list_filter( $groups, array( 'listed' => true ) ) );
foreach ( $groups as $index => $group ) {
$groups[ $index ]['own_file'] = '' !== $group['anchor']
&& ( ! $group['listed'] || $listed > 1 );
}

$cache[ $post->ID ] = array(
'content' => $post->post_content,
'groups' => $groups,
Expand All @@ -205,14 +224,64 @@ public static function extract_groups( $post ) {
}

/**
* Whether a page has more than one blogroll, so that each one is a
* group with an address of its own. A single blogroll is the page.
* The groups that make up the file of a page.
*
* A list can be unlisted and still be subscribed to on its own, for a
* page that has one list for its readers and another one that is only
* of interest to whoever is on the page.
*
* @param \WP_Post $post Post object.
* @return bool True with two or more blogrolls.
* @return array Groups.
*/
public static function listed_groups( $post ) {
return \array_values( \wp_list_filter( self::all_groups( $post ), array( 'listed' => true ) ) );
}

/**
* The address of the file a group is served under: its own, or the
* one of the page it shares.
*
* Read here and not kept with the group: all_groups() is cached on the
* content of a post, and an address also depends on its permalink.
*
* @param \WP_Post $post Post object.
* @param array $group Group as returned by all_groups().
* @return string OPML URL.
*/
public static function group_url( $post, $group ) {
return self::opml_url( $post, $group['own_file'] ? $group['anchor'] : '' );
}

/**
* One blogroll of a page, by its HTML anchor.
*
* @param \WP_Post $post Post object.
* @param string $anchor HTML anchor of one blogroll block.
* @return array|null The group, or null when the page has no such one.
*/
public static function group( $post, $anchor ) {
if ( '' === $anchor ) {
return null;
}

$group = \wp_list_filter( self::all_groups( $post ), array( 'anchor' => $anchor ) );

return 1 === \count( $group ) ? \reset( $group ) : null;
}

/**
* The groups an opml request asks for: one list, or the file of the
* page. An anchor no block has falls back to the file of the page,
* rather than an error.
*
* @param \WP_Post $post Post object.
* @param string $anchor HTML anchor of one blogroll block, or empty.
* @return array Groups, empty when there is nothing to serve.
*/
public static function is_grouped( $post ) {
return \count( self::extract_groups( $post ) ) > 1;
public static function requested_groups( $post, $anchor = '' ) {
$group = self::group( $post, $anchor );

return $group ? array( $group ) : self::listed_groups( $post );
}

/**
Expand All @@ -223,7 +292,7 @@ public static function is_grouped( $post ) {
* to the name of the block itself. A single blogroll stays a plain list,
* the page is its own group.
*
* @param array $groups Groups as returned by extract_groups().
* @param array $groups Groups as returned by all_groups().
* @return string The escaped elements.
*/
public static function outlines( $groups ) {
Expand Down Expand Up @@ -271,7 +340,7 @@ private static function link_outline( $link, $indent = "\t\t" ) {
/**
* Name of a group, falling back to the name of the block itself.
*
* @param array $group Group as returned by extract_groups().
* @param array $group Group as returned by all_groups().
* @return string Name.
*/
private static function group_name( $group ) {
Expand All @@ -281,7 +350,7 @@ private static function group_name( $group ) {
/**
* Title of one blogroll of a page: its name, then the page title.
*
* @param array $group Group as returned by extract_groups().
* @param array $group Group as returned by all_groups().
* @param string $page_title Title of the page, see title().
* @return string Title.
*/
Expand All @@ -304,13 +373,11 @@ private static function group_title( $group, $page_title ) {
* @param string $anchor HTML anchor of one blogroll block, or empty for all.
*/
public static function for_post( $post, $anchor = '' ) {
$groups = self::extract_groups( $post );
$title = self::title( $post );

$group = '' !== $anchor ? \wp_list_filter( $groups, array( 'anchor' => $anchor ) ) : array();
if ( 1 === \count( $group ) ) {
$groups = array( \reset( $group ) );
$title = self::group_title( $groups[0], $title );
$group = self::group( $post, $anchor );
$groups = $group ? array( $group ) : self::listed_groups( $post );
if ( $group ) {
$title = self::group_title( $group, $title );
}

\load_template(
Expand Down Expand Up @@ -372,16 +439,24 @@ public static function render() {

// The well-known URL asks for the directory, whatever page it lands on.
$directory = self::DIRECTORY === $opml;
$anchor = (string) \get_query_var( self::GROUP, '' );
$post = $directory ? null : Index::queried_post();
$posts = ( ! $post && ( $directory || self::is_blogroll_root() ) ) ? Index::get_posts() : array();
// A page whose lists are all unlisted has no file of its own, and
// neither has one that is asked for a list it does not hold. It is
// then a page without a blogroll: the root still answers with the
// directory, every other page simply loads.
if ( $post && ! self::requested_groups( $post, $anchor ) ) {
$post = null;
}
$posts = ( ! $post && ( $directory || self::is_blogroll_root() ) ) ? Index::get_posts() : array();

if ( ! $post && ! $posts ) {
return;
}

\header( 'Content-Type: text/xml; charset=' . \get_option( 'blog_charset' ) );
if ( $post ) {
self::for_post( $post, (string) \get_query_var( self::GROUP, '' ) );
self::for_post( $post, $anchor );
} else {
self::directory( $posts );
}
Expand Down Expand Up @@ -438,7 +513,9 @@ public static function discovery_link() {
$post = Index::queried_post();
if ( $post ) {
$title = self::title( $post );
self::print_links( self::opml_url( $post ), \get_permalink( $post ), $title );
if ( self::listed_groups( $post ) ) {
self::print_links( self::opml_url( $post ), \get_permalink( $post ), $title );
}
self::print_group_links( $post, $title );
}

Expand Down Expand Up @@ -470,21 +547,23 @@ private static function print_discovery_link( $post ) {
/**
* Print the rel="blogroll" links of the single blogrolls on a page.
*
* Only a page with more than one gets them, see is_grouped().
* A list that is the file of its page shares its address and needs no
* link of its own; every other list gets one.
*
* @param \WP_Post $post Post with blogroll blocks.
* @param string $page_title Title of the page, see title().
*/
private static function print_group_links( $post, $page_title ) {
$groups = self::extract_groups( $post );
if ( \count( $groups ) < 2 ) {
return;
}

$permalink = \get_permalink( $post );
foreach ( $groups as $group ) {
foreach ( self::all_groups( $post ) as $group ) {
// A single listed list is the file of the page, which the page
// advertises itself; it needs no link of its own.
if ( ! $group['own_file'] ) {
continue;
}

self::print_links(
self::opml_url( $post, $group['anchor'] ),
self::group_url( $post, $group ),
$permalink . '#' . $group['anchor'],
self::group_title( $group, $page_title )
);
Expand Down
Loading
Loading