, Open Graph, hreflang, shipping, ratings, product identifiers and the
* questions a GenScore recommendation asks a page to answer.
*
* Same rules as the first fixes: every proposal comes from data WordPress or
* WooCommerce already has, or the person writes it; the post content is never
* rewritten (headings and questions are served by filters); every publish
* keeps the previous value so it can be undone.
*
* @package GenScore
*/
defined( 'ABSPATH' ) || exit;
/** Shipping data shared by the products that publish it. */
const GENSCORE_SHIPPING_OPTION = 'genscore_shipping';
/** Headings proposed or typed for one page, at most. */
const GENSCORE_H2_MAX = 6;
/** Questions a page answers through «Añadir preguntas a la página», at most. */
const GENSCORE_FAQ_MAX = 8;
/**
* The 1.4.0 kinds: post types each one applies to. None of them is part of
* the page review (score.js), so their status is worked out here.
*
* @return array
*/
function genscore_more_kinds() {
return array(
'h2' => array( 'types' => array( 'page', 'post', 'product' ), 'ids' => array() ),
'h1' => array( 'types' => array( 'page', 'post', 'product' ), 'ids' => array() ),
'og' => array( 'types' => array( 'page', 'post', 'product' ), 'ids' => array() ),
'hreflang' => array( 'types' => array( 'page', 'post', 'product' ), 'ids' => array() ),
'shipping' => array( 'types' => array( 'product' ), 'ids' => array() ),
'rating' => array( 'types' => array( 'product' ), 'ids' => array() ),
'gtin' => array( 'types' => array( 'product' ), 'ids' => array() ),
'faq' => array( 'types' => array( 'page', 'post', 'product' ), 'ids' => array() ),
);
}
/**
* Whether a kind is one of these.
*
* @param string $kind Kind.
* @return bool
*/
function genscore_is_more_kind( $kind ) {
return isset( genscore_more_kinds()[ $kind ] );
}
/**
* The plugin that already handles languages, if any.
*
* @return string|null
*/
function genscore_multilingual_plugin() {
if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
return 'WPML';
}
if ( function_exists( 'pll_the_languages' ) ) {
return 'Polylang';
}
if ( defined( 'TRP_PLUGIN_VERSION' ) ) {
return 'TranslatePress';
}
return null;
}
/**
* Whether one of these kinds can be published here, and if not, why.
*
* @param string $kind Kind.
* @return string|true
*/
function genscore_more_available( $kind ) {
$woo = class_exists( 'WooCommerce' ) && function_exists( 'wc_get_product' );
if ( in_array( $kind, array( 'shipping', 'rating', 'gtin' ), true ) && ! $woo ) {
return __( 'Hace falta WooCommerce.', 'genscore' );
}
if ( 'rating' === $kind && ! genscore_any_rated_product() ) {
return __( 'Sin reseñas: la valoración sólo se publica con reseñas reales de tus clientes.', 'genscore' );
}
if ( 'og' === $kind && genscore_seo_plugin() ) {
$seo = genscore_seo_plugin();
/* translators: %s: SEO plugin name. */
return sprintf( __( '%s ya publica Open Graph: actívalo en sus ajustes de redes sociales.', 'genscore' ), $seo['name'] );
}
if ( 'hreflang' === $kind && genscore_multilingual_plugin() ) {
/* translators: %s: multilingual plugin name. */
return sprintf( __( '%s ya gestiona los idiomas de tu web: revisa sus ajustes.', 'genscore' ), genscore_multilingual_plugin() );
}
return true;
}
/**
* Whether any published product has a real rating to declare.
*
* @return bool
*/
function genscore_any_rated_product() {
$ids = get_posts(
array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- one row, admin screen only.
'meta_query' => array(
array(
'key' => '_wc_review_count',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
),
),
)
);
return ! empty( $ids );
}
/* ------------------------------------------------------------------ */
/* Section headings */
/* ------------------------------------------------------------------ */
/**
* The page's stored content. Block markup already holds its headings and
* paragraphs as HTML; rendering every block for a list of hundreds of pages
* would be too slow.
*
* @param int $id Post id.
* @return string
*/
function genscore_raw_content( $id ) {
$post = $id ? get_post( $id ) : null;
return $post ? (string) $post->post_content : '';
}
/**
*
headings already in the content.
*
* @param string $html HTML.
* @return int
*/
function genscore_h2_count( $html ) {
return (int) preg_match_all( '#
]#i', (string) $html );
}
/**
* Paragraphs that are only a short line in bold: the page's own headings,
* written as text instead of as headings.
*
* @param string $html HTML.
* @return string[] Their text.
*/
function genscore_bold_lines( $html ) {
$out = array();
if ( preg_match_all( '#
]*>\s*<(strong|b)>([^<]{3,80})\1>\s*:?\s*
#i', (string) $html, $m ) ) {
foreach ( $m[2] as $t ) {
$t = trim( html_entity_decode( $t, ENT_QUOTES, 'UTF-8' ) );
if ( '' !== $t && ! preg_match( '/[.,;]$/u', $t ) && ! in_array( $t, $out, true ) ) {
$out[] = $t;
}
}
}
return array_slice( $out, 0, GENSCORE_H2_MAX );
}
/**
* Cleans typed headings: one per line, 2 to 80 characters, as many as the
* page needs to reach two.
*
* @param string $value Text.
* @param int $id Post id.
* @return string|null
*/
function genscore_clean_h2( $value, $id ) {
$lines = array();
foreach ( preg_split( '/\r\n|\r|\n/', (string) $value ) as $l ) {
$l = sanitize_text_field( $l );
if ( mb_strlen( $l ) >= 2 && ! in_array( $l, $lines, true ) ) {
$lines[] = mb_substr( $l, 0, 80 );
}
}
$lines = array_slice( $lines, 0, GENSCORE_H2_MAX );
$need = max( 1, 2 - genscore_h2_count( genscore_raw_content( $id ) ) );
return count( $lines ) >= $need ? implode( "\n", $lines ) : null;
}
/**
* Serves the stored headings: a bold line with the same text becomes an
*
; the rest go before paragraphs spread through the page.
*
* @param string $html Content.
* @param string $value Stored headings, one per line.
* @return string
*/
function genscore_apply_h2( $html, $value ) {
$lines = array_values( array_filter( array_map( 'trim', explode( "\n", (string) $value ) ) ) );
$rest = array();
foreach ( $lines as $l ) {
$re = '#
]#i', $html, $m, PREG_OFFSET_CAPTURE );
$starts = wp_list_pluck( $m[0], 1 );
$p = count( $starts );
$n = count( $rest );
if ( $p < 2 ) {
// Too little text to place headings inside it: they go at the end, never invented between words.
foreach ( $rest as $l ) {
$html .= '