Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
/**
* The Block Admin
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema\Blocks;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Arr;
defined( 'ABSPATH' ) || exit;
/**
* Block Admin class.
*/
class Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/settings/general', 'add_general_settings' );
}
/**
* Add block settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_general_settings( $tabs ) {
Arr::insert(
$tabs,
[
'blocks' => [
'icon' => 'rm-icon rm-icon-stories',
'title' => esc_html__( 'Blocks', 'rank-math' ),
'desc' => esc_html__( 'Take control over the default settings available for Rank Math Blocks.', 'rank-math' ),
'file' => __DIR__ . '/views/options-general.php',
],
],
7
);
return $tabs;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* The Block Parser
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Block_Parser class.
*/
class Block_Parser {
use Hooker;
/**
* Holds the parsed blocks.
*
* @var array
*/
private $blocks = [];
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/json_ld', 'parse', 8 );
}
/**
* Filter function to add Blocks data in schema.
*
* @param array $data Array of JSON-LD data.
*
* @return array
*/
public function parse( $data ) {
if ( ! function_exists( 'parse_blocks' ) || ! is_singular() ) {
return $data;
}
$this->get_parsed_blocks();
foreach ( $this->blocks as $block_type => $blocks ) {
foreach ( $blocks as $block ) {
/**
* Filter: 'rank_math/schema/block/<block-type>' - Allows filtering graph output per block.
*
* @param array $data Array of JSON-LD data.
* @param array $block The block.
*/
$data = $this->do_filter( 'schema/block/' . $block_type, $data, $block );
}
}
return $data;
}
/**
* Parse the blocks and loop through them.
*/
private function get_parsed_blocks() {
$post = get_post();
$parsed_blocks = parse_blocks( $post->post_content );
/**
* Filter: 'rank_math/schema/block/before_filter'
*
* @param array $parsed_blocks Array of parsed blocks.
*/
$parsed_blocks = $this->do_filter( 'schema/block/before_filter', $parsed_blocks );
$this->filter_blocks( $parsed_blocks );
}
/**
* Filter blocks.
*
* @param array $blocks Blocks to filter.
*/
private function filter_blocks( $blocks ) {
foreach ( $blocks as $block ) {
if ( $this->is_nested_block( $block ) || ! $this->is_valid_block( $block ) ) {
continue;
}
$name = \str_replace( 'rank-math/', '', $block['blockName'] );
$name = strtolower( $name );
if ( ! isset( $this->blocks[ $name ] ) || ! is_array( $this->blocks[ $name ] ) ) {
$this->blocks[ $name ] = [];
}
$this->blocks[ $name ][] = $block;
}
}
/**
* Is nested block.
*
* @param array $block Block.
*
* @return boolean
*/
private function is_nested_block( $block ) {
if ( empty( $block['blockName'] ) ) {
return false;
}
/**
* Filter: 'rank_math/schema/nested_blocks' - Allows filtering for nested blocks.
*
* @param array $data Array of JSON-LD data.
* @param array $block The block.
*/
$nested = $this->do_filter(
'schema/nested_blocks',
[
'core/group',
'core/columns',
'core/column',
]
);
if ( ! in_array( $block['blockName'], $nested, true ) ) {
return false;
}
$this->filter_blocks( $block['innerBlocks'] );
return true;
}
/**
* Is block valid.
*
* @param array $block Block.
*
* @return boolean
*/
private function is_valid_block( $block ) {
return ! empty( $block['blockName'] ) && Str::starts_with( 'rank-math', $block['blockName'] );
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* The Block Base
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Block class.
*/
class Block {
/**
* Function to certain tags from the text.
*
* @param string $text Block content.
*
* @return string
*/
protected function clean_text( $text ) {
return strip_tags( $text, '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' );
}
/**
* Function to get the block image.
*
* @param array $attrs Block attributes data.
* @param string $size Image size.
* @param string $image_class Attachment image class.
*
* @return string The HTML image element.
*/
protected function get_image( $attrs, $size = 'thumbnail', $image_class = 'class=alignright' ) {
$image_id = empty( $attrs['imageID'] ) ? '' : absint( $attrs['imageID'] );
if ( ! $image_id ) {
return '';
}
$html = wp_get_attachment_image( $image_id, $size, false, $image_class );
return $html ? $html : wp_get_attachment_image( $image_id, 'full', false, $image_class );
}
/**
* Get styles
*
* @param array $attributes Array of attributes.
*
* @return string
*/
protected function get_styles( $attributes ) {
return empty( $attributes['textAlign'] ) || ! in_array( $attributes['textAlign'], [ 'right', 'center' ], true )
? ''
: ' style="text-align:' . $attributes['textAlign'] . '"';
}
/**
* Get list style
*
* @param string $style Style.
*
* @return string
*/
protected function get_list_style( $style ) {
if ( 'numbered' === $style ) {
return 'ol';
}
return 'unordered' === $style ? 'ul' : 'div';
}
/**
* Get list item style
*
* @param string $style Style.
*
* @return string
*/
protected function get_list_item_style( $style ) {
return in_array( $style, [ 'numbered', 'unordered' ], true ) ? 'li' : 'div';
}
/**
* Get title wrapper tag.
*
* @param string $title_wrapper Title wrapper attribute.
* @param string $block Block name.
*/
protected function get_title_wrapper( $title_wrapper, $block = 'faq' ) {
$wrapper = in_array( $title_wrapper, [ 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div' ], true ) ? $title_wrapper : 'h2';
return apply_filters( "rank_math/blocks/{$block}/title_wrapper", $wrapper );
}
/**
* Normalize the block text.
*
* @param string $text Text.
* @param string $block Block name.
*
* @return string
*/
protected function normalize_text( $text, $block ) {
/**
* Filter: Allow developers to preserve line breaks.
*
* @param bool $return If set, this will convert all remaining line breaks after paragraphing.
* @param string $block Block name.
*/
return wpautop( wp_kses_post( $text ), apply_filters( 'rank_math/block/preserve_line_breaks', true, $block ) );
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { map } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { withSelect } from '@wordpress/data'
import { InspectorControls } from '@wordpress/block-editor'
import { PanelBody, SelectControl, TextControl } from '@wordpress/components'
/**
* Format array of image sizes.
*
* @param {Array} imageSizes Array of image sizes.
* @return {Array} Formatted array.
*/
const getImageSizeOptions = ( imageSizes ) => {
return map( imageSizes, ( { name, slug } ) => ( {
value: slug,
label: name,
} ) )
}
/**
* Adds controls to the editor sidebar to control params.
*
* @param {Object} props This component's props.
*/
const Inspector = ( { imageSizes, attributes, setAttributes } ) => {
const imageSizeOptions = getImageSizeOptions( imageSizes )
return (
<InspectorControls key={ 'inspector' }>
<PanelBody title={ __( 'FAQ Options', 'rank-math' ) }>
<SelectControl
label={ __( 'List Style', 'rank-math' ) }
value={ attributes.listStyle }
options={ [
{
value: '',
label: __( 'None', 'rank-math' ),
},
{
value: 'numbered',
label: __( 'Numbered', 'rank-math' ),
},
{
value: 'unordered',
label: __( 'Unordered', 'rank-math' ),
},
] }
onChange={ ( listStyle ) => {
setAttributes( { listStyle } )
} }
/>
<SelectControl
label={ __( 'Title Wrapper', 'rank-math' ) }
value={ attributes.titleWrapper }
options={ [
{ value: 'h2', label: __( 'H2', 'rank-math' ) },
{ value: 'h3', label: __( 'H3', 'rank-math' ) },
{ value: 'h4', label: __( 'H4', 'rank-math' ) },
{ value: 'h5', label: __( 'H5', 'rank-math' ) },
{ value: 'h6', label: __( 'H6', 'rank-math' ) },
{ value: 'p', label: __( 'P', 'rank-math' ) },
{ value: 'div', label: __( 'DIV', 'rank-math' ) },
] }
onChange={ ( titleWrapper ) => {
setAttributes( { titleWrapper } )
} }
/>
<SelectControl
label={ __( 'Image Size', 'rank-math' ) }
value={ attributes.sizeSlug }
options={ imageSizeOptions }
onChange={ ( sizeSlug ) => {
setAttributes( { sizeSlug } )
} }
/>
</PanelBody>
<PanelBody title={ __( 'Styling Options', 'rank-math' ) }>
<TextControl
label={ __( 'Title Wrapper CSS Class(es)', 'rank-math' ) }
value={ attributes.titleCssClasses }
onChange={ ( titleCssClasses ) => {
setAttributes( { titleCssClasses } )
} }
/>
<TextControl
label={ __( 'Content Wrapper CSS Class(es)', 'rank-math' ) }
value={ attributes.contentCssClasses }
onChange={ ( contentCssClasses ) => {
setAttributes( { contentCssClasses } )
} }
/>
<TextControl
label={ __( 'List CSS Class(es)', 'rank-math' ) }
value={ attributes.listCssClasses }
onChange={ ( listCssClasses ) => {
setAttributes( { listCssClasses } )
} }
/>
</PanelBody>
</InspectorControls>
)
}
export default withSelect( ( select, props ) => {
const { getSettings } = select( 'core/block-editor' )
const { imageSizes } = getSettings()
return {
...props,
imageSizes,
}
} )( Inspector )

View File

@@ -0,0 +1,155 @@
/**
* External dependencies
*/
import classnames from 'classnames'
/**
* Internal dependencies
*/
import MediaUploader from '@blocks/shared/MediaUploader'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { applyFilters } from '@wordpress/hooks'
import { Button } from '@wordpress/components'
import { Component } from '@wordpress/element'
import { RichText, MediaUpload } from '@wordpress/block-editor'
/**
* A Question and answer pair within FAQ block.
*/
class Question extends Component {
/**
* Renders the component.
*
* @return {Component} Question editor.
*/
render() {
const {
title,
content,
visible,
imageID,
sizeSlug,
titleWrapper,
titleCssClasses,
contentCssClasses,
} = this.props
const wrapperClasses = classnames( 'rank-math-question-wrapper', {
'question-not-visible': ! visible,
} )
return (
<div className={ wrapperClasses }>
<div className="rank-math-item-header">
<RichText
tagName={ titleWrapper }
className={
'rank-math-faq-question rank-math-block-title' +
titleCssClasses
}
value={ title }
onChange={ ( newTitle ) => {
this.setQuestionProp( 'title', newTitle )
} }
placeholder={ __( 'Question…', 'rank-math' ) }
/>
<div className="rank-math-block-actions">
{ applyFilters( 'rank_math_block_faq_actions', '', this.props, this ) }
<Button
className="rank-math-item-visbility"
icon={ visible ? 'visibility' : 'hidden' }
onClick={ this.toggleVisibility }
label={ __( 'Hide Question', 'rank-math' ) }
showTooltip={ true }
/>
<Button
icon="trash"
className="rank-math-item-delete"
onClick={ this.deleteQuestion }
label={ __( 'Delete Question', 'rank-math' ) }
showTooltip={ true }
/>
</div>
</div>
<div className="rank-math-item-content">
<RichText
tagName="div"
className={ 'rank-math-faq-answer ' + contentCssClasses }
value={ content }
onChange={ ( newContent ) => {
this.setQuestionProp( 'content', newContent )
} }
placeholder={ __(
'Enter the answer to the question',
'rank-math'
) }
/>
<MediaUpload
allowedTypes={ [ 'image' ] }
multiple={ false }
value={ imageID }
render={ ( { open } ) => (
<MediaUploader
imageID={ imageID }
sizeSlug={ sizeSlug }
open={ open }
removeImage={ () => {
this.setQuestionProp( 'imageID', 0 )
} }
/>
) }
onSelect={ ( image ) => {
this.setQuestionProp( 'imageID', image.id )
} }
/>
</div>
</div>
)
}
/**
* Update question properties.
*
* @param {string} prop Poperty name.
* @param {string} value Property value.
*/
setQuestionProp( prop, value ) {
const { setAttributes, index } = this.props
const questions = [ ...this.props.questions ]
questions[ index ][ prop ] = value
setAttributes( { questions } )
}
/**
* Toggle question visibility.
*/
toggleVisibility = () => {
const { setAttributes, index } = this.props
const questions = [ ...this.props.questions ]
questions[ index ].visible = ! this.props.visible
setAttributes( { questions } )
}
/**
* Delete question from block.
*/
deleteQuestion = () => {
const { setAttributes, index } = this.props
const questions = [ ...this.props.questions ]
questions.splice( index, 1 )
setAttributes( { questions } )
}
}
export default Question

View File

@@ -0,0 +1,141 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Fragment } from '@wordpress/element'
import { Button, Dashicon } from '@wordpress/components'
import { BlockControls, AlignmentToolbar, useBlockProps } from '@wordpress/block-editor'
/**
* Internal dependencies
*/
import getLink from '@helpers/getLink'
import Inspector from './components/Inspector'
import Question from './components/Question'
import generateId from '@helpers/generateId'
/**
* Render Quetion component.
*
* @param {Object} props Block attributes
*
* @return {Array} Array of question editor.
*/
const renderQuestions = ( props ) => {
const {
sizeSlug,
titleWrapper,
titleCssClasses,
contentCssClasses,
} = props.attributes
let { questions } = props.attributes
if ( isEmpty( questions ) ) {
questions = [
{
id: generateId( 'faq-question' ),
title: '',
content: '',
visible: true,
},
]
props.setAttributes( { questions } )
}
return questions.map( ( question, index ) => {
return (
<li key={ question.id }>
<Question
{ ...question }
index={ index }
key={ question.id + '-question' }
questions={ questions }
setAttributes={ props.setAttributes }
sizeSlug={ sizeSlug }
titleWrapper={ titleWrapper }
titleCssClasses={ titleCssClasses }
contentCssClasses={ contentCssClasses }
/>
</li>
)
} )
}
/**
* Add an empty Question into block.
*
* @param {Object} props Block props.
*/
const addNew = ( props ) => {
const questions = [ ...props.attributes.questions ]
questions.push( {
id: generateId( 'faq-question' ),
title: '',
content: '',
visible: true,
} )
props.setAttributes( { questions } )
}
/**
* FAQ block edit component.
*
* @param {Object} props Block props.
*/
export default ( props ) => {
const { className, isSelected } = props
const { textAlign } = props.attributes
const blockProps = useBlockProps()
return (
<div { ...blockProps }>
<div
id="rank-math-faq"
className={ 'rank-math-block ' + className }
>
{ isSelected && <Inspector { ...props } /> }
{ isSelected && (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ textAlign }
onChange={ ( nextTextAlignment ) =>
props.setAttributes( {
textAlign: nextTextAlignment,
} )
}
/>
</BlockControls>
</Fragment>
) }
<ul style={ { textAlign } }>{ renderQuestions( props ) }</ul>
<Button
variant="primary"
onClick={ () => {
addNew( props )
} }
>
{ __( 'Add New FAQ', 'rank-math' ) }
</Button>
<a
href={ getLink( 'faq-schema-block', 'Add New FAQ' ) }
rel="noopener noreferrer"
target="_blank"
title={ __( 'More Info', 'rank-math' ) }
className={ 'rank-math-block-info' }
>
<Dashicon icon="info" />
</a>
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
/**
* FAQ eample for in block preview pane.
*
* @type {Object}
*/
export default {
attributes: {
questions: [
{
visible: true,
titleWrapper: 'div',
title: 'Question',
content:
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
},
],
},
}

View File

@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import example from './example'
import edit from './edit'
import transforms from './transforms'
import save from './save'
/**
* Register FAQ block.
*/
registerBlockType(
'rank-math/faq-block',
{
example,
edit,
save,
transforms,
}
)

View File

@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { RichText, useBlockProps } from '@wordpress/block-editor'
/**
* Save block for display on front
*
* @param {Object} props This component's props.
*/
export default ( props ) => {
const { questions, titleWrapper } = props.attributes
if ( isEmpty( questions ) ) {
return null
}
return (
<div { ...useBlockProps.save() }>
{ questions.map( ( question, index ) => {
if (
isEmpty( question.title ) ||
isEmpty( question.content ) ||
false === question.visible
) {
return null
}
return (
<div className="rank-math-faq-item" key={ index }>
<RichText.Content
tagName={ titleWrapper }
value={ question.title }
className="rank-math-question"
/>
<RichText.Content
tagName="div"
value={ question.content }
className="rank-math-answer"
/>
</div>
)
} ) }
</div>
)
}

View File

@@ -0,0 +1,35 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks'
/**
* Transform yoast faq block.
*
* @type {Array}
*/
export default {
from: [
{
type: 'block',
blocks: [ 'yoast/faq-block' ],
transform: ( yoast ) => {
const questions = yoast.questions.map( ( question ) => {
return {
title: question.jsonQuestion,
content: question.jsonAnswer,
visible: true,
}
} )
const attributes = {
titleWrapper: 'h3',
questions,
className: yoast.className,
}
return createBlock( 'rank-math/faq-block', attributes )
},
},
],
}

View File

@@ -0,0 +1,52 @@
{
"apiVersion": 3,
"title": "FAQ by Rank Math",
"description": "Easily add Schema-ready, SEO-friendly, Frequently Asked Questions to your content.",
"name": "rank-math/faq-block",
"category": "rank-math-blocks",
"icon": "editor-ul",
"textdomain": "rank-math",
"keywords": [ "FAQ", "Frequently Asked Questions", "Schema", "SEO", "Structured Data", "Yoast", "Rank Math", "Block", "Markup", "Rich Snippet" ],
"editorScript": [
"lodash",
"file:./assets/js/index.js"
],
"editorStyle": "rank-math-block-admin",
"attributes": {
"listStyle": {
"type": "string",
"default": ""
},
"titleWrapper": {
"type": "string",
"default": "h3"
},
"sizeSlug": {
"type": "string",
"default": "thumbnail"
},
"questions": {
"type": "array",
"default": [],
"items": {
"type": "object"
}
},
"listCssClasses": {
"type": "string",
"default": ""
},
"titleCssClasses": {
"type": "string",
"default": ""
},
"contentCssClasses": {
"type": "string",
"default": ""
},
"textAlign": {
"type": "string",
"default": "left"
}
}
}

View File

@@ -0,0 +1,210 @@
<?php
/**
* The Faq Block
*
* @since 1.0.233
* @package RankMath
* @subpackage RankMath\Faq
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Faq Block class.
*/
class Block_FAQ extends Block {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/faq-block';
/**
* The single instance of the class.
*
* @var Block_FAQ
*/
protected static $instance = null;
/**
* Retrieve main Block_FAQ instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_FAQ
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_FAQ ) ) {
self::$instance = new Block_FAQ();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type(
RANK_MATH_PATH . 'includes/modules/schema/blocks/faq/block.json',
[
'render_callback' => [ $this, 'render' ],
]
);
add_filter( 'rank_math/schema/block/faq-block', [ $this, 'add_graph' ], 10, 2 );
}
/**
* Add FAQ schema data in JSON-LD array.
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
// Early bail.
if ( ! $this->has_questions( $block['attrs'] ) ) {
return $data;
}
if ( ! isset( $data['faqs'] ) ) {
$data['faqs'] = [
'@type' => 'FAQPage',
'mainEntity' => [],
];
}
$permalink = get_permalink() . '#';
foreach ( $block['attrs']['questions'] as $question ) {
if ( empty( $question['title'] ) || empty( $question['content'] ) || empty( $question['visible'] ) ) {
continue;
}
$question['title'] = do_shortcode( $question['title'] );
$question['content'] = do_shortcode( $question['content'] );
if ( empty( $question['id'] ) ) {
$question['id'] = 'rm-faq-' . md5( $question['title'] );
}
$data['faqs']['mainEntity'][] = [
'@type' => 'Question',
'url' => esc_url( $permalink . $question['id'] ),
'name' => wp_strip_all_tags( $question['title'] ),
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $this->clean_text( $question['content'] ),
],
];
}
return $data;
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
* @return string
*/
public static function markup( $attributes = [] ) {
$list_tag = self::get()->get_list_style( $attributes['listStyle'] );
$item_tag = self::get()->get_list_item_style( $attributes['listStyle'] );
$class = 'rank-math-block';
if ( ! empty( $attributes['className'] ) ) {
$class .= ' ' . esc_attr( $attributes['className'] );
}
// HTML.
$out = [];
$out[] = sprintf( '<div id="rank-math-faq" class="%1$s"%2$s>', $class, self::get()->get_styles( $attributes ) );
$out[] = sprintf( '<%1$s class="rank-math-list %2$s">', $list_tag, esc_attr( $attributes['listCssClasses'] ) );
// Questions.
foreach ( $attributes['questions'] as $question ) {
if ( empty( $question['title'] ) || empty( $question['content'] ) || empty( $question['visible'] ) ) {
continue;
}
if ( empty( $question['id'] ) ) {
$question['id'] = 'rm-faq-' . md5( $question['title'] );
}
$out[] = sprintf( '<%1$s id="%2$s" class="rank-math-list-item">', $item_tag, esc_attr( $question['id'] ) );
$out[] = sprintf(
'<%1$s class="rank-math-question %2$s">%3$s</%1$s>',
self::get()->get_title_wrapper( $attributes['titleWrapper'] ),
esc_attr( $attributes['titleCssClasses'] ),
wp_kses_post( $question['title'] )
);
$out[] = '<div class="rank-math-answer ' . esc_attr( $attributes['contentCssClasses'] ) . '">';
if ( ! empty( $question['imageUrl'] ) ) {
$out[] = '<img src="' . esc_url( $question['imageUrl'] ) . '" />';
} else {
$out[] = self::get()->get_image( $question, $attributes['sizeSlug'] );
}
$out[] = self::get()->normalize_text( $question['content'], 'faq' );
$out[] = '</div>';
$out[] = sprintf( '</%1$s>', $item_tag );
}
$out[] = sprintf( '</%1$s>', $list_tag );
$out[] = '</div>';
return apply_filters(
'rank_math/schema/block/faq/content',
wp_kses_post( join( "\n", $out ) ),
$out,
$attributes
);
}
/**
* Render block content
*
* @param array $attributes Array of atributes.
*
* @return string
*/
public function render( $attributes ) {
// Early bail.
if ( ! $this->has_questions( $attributes ) ) {
return '';
}
return self::markup( $attributes );
}
/**
* Check if FAQ block has questions data.
*
* @param array $attributes Array of attributes.
*
* @return boolean
*/
private function has_questions( $attributes ) {
return ! isset( $attributes['questions'] ) || empty( $attributes['questions'] ) ? false : true;
}
}

View File

@@ -0,0 +1 @@
@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(-360deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(-360deg)}}@keyframes bounce{from{-webkit-transform:translateY(0px);transform:translateY(0px)}to{-webkit-transform:translateY(-5px);transform:translateY(-5px)}}@-webkit-keyframes bounce{from{-webkit-transform:translateY(0px);transform:translateY(0px)}to{-webkit-transform:translateY(-5px);transform:translateY(-5px)}}@-webkit-keyframes loading{0%{background-size:20% 50%,20% 50%,20% 50%}20%{background-size:20% 20%,20% 50%,20% 50%}40%{background-size:20% 100%,20% 20%,20% 50%}60%{background-size:20% 50%,20% 100%,20% 20%}80%{background-size:20% 50%,20% 50%,20% 100%}100%{background-size:20% 50%,20% 50%,20% 50%}}@keyframes loading{0%{background-size:20% 50%,20% 50%,20% 50%}20%{background-size:20% 20%,20% 50%,20% 50%}40%{background-size:20% 100%,20% 20%,20% 50%}60%{background-size:20% 50%,20% 100%,20% 20%}80%{background-size:20% 50%,20% 50%,20% 100%}100%{background-size:20% 50%,20% 50%,20% 50%}}:root{--rankmath-wp-adminbar-height: 0}#rank-math-howto .rank-math-media-placeholder{position:relative;display:inline-block;float:none;margin-bottom:15px;margin-left:0;text-align:left}#rank-math-howto .components-base-control__label{font-weight:600}#rank-math-howto .components-base-control__field{margin-bottom:0}#rank-math-howto .components-base-control__help{font-style:italic;margin:0}#rank-math-howto .components-text-control__input,#rank-math-howto .components-textarea-control__input{font-size:1rem;margin-bottom:0;color:#3a3a3a;border-color:#b5bfc9}#rank-math-howto .rank-math-howto-duration-label .components-toggle-control{display:inline-block;margin-left:8px}#rank-math-howto .rank-math-howto-duration-fields{margin-top:10px}#rank-math-howto .rank-math-howto-duration-fields .components-base-control{display:inline-block;width:75px;margin-right:5px}#rank-math-howto .rank-math-howto-duration-fields .components-base-control+.components-base-control{margin-bottom:0 !important}#rank-math-howto .rank-math-howto-duration-fields .components-base-control>.components-base-control__field{margin:0}#rank-math-howto .rank-math-howto-duration-fields .components-base-control:first-child{width:auto}#rank-math-howto .rank-math-howto-duration-fields .components-base-control.hidden{display:none}#rank-math-howto .rank-math-howto-duration-instructions{font-size:13px;font-style:italic;margin:10px 0 5px;opacity:.7}#rank-math-howto .rank-math-howto-estimated-cost{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}#rank-math-howto .rank-math-howto-estimated-cost>div:last-child{margin-left:10px;-ms-flex-item-align:end;align-self:flex-end}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
// Common and Vendors
@import '../../../../../../../assets/vendor/bourbon/bourbon';
@import '../../../../../../../assets/admin/scss/mixins';
@import '../../../../../../../assets/admin/scss/variables';
// HowTo Block.
#rank-math-howto {
.rank-math-media-placeholder {
position: relative;
display: inline-block;
float: none;
margin-bottom: 15px;
margin-left: 0;
text-align: left;
}
.components-base-control {
&__label {
font-weight: 600;
}
&__field {
margin-bottom: 0;
}
&__help {
font-style: italic;
margin: 0;
}
}
.components-text-control__input,
.components-textarea-control__input {
font-size: 1rem;
margin-bottom: 0;
color: #3a3a3a;
border-color: $gray;
}
.rank-math-howto {
&-duration {
&-label {
// Toggle Control.
.components-toggle-control {
display: inline-block;
margin-left: 8px;
}
}
&-fields {
margin-top: 10px;
.components-base-control {
display: inline-block;
width: 75px;
margin-right: 5px;
+ .components-base-control {
margin-bottom: 0!important;
}
> .components-base-control__field {
margin: 0;
}
&:first-child {
width: auto;
}
&.hidden {
display: none;
}
}
}
&-instructions {
font-size: 13px;
font-style: italic;
margin: 10px 0 5px;
opacity: .7;
}
}
&-estimated-cost {
display: flex;
flex-flow: row wrap;
> div:last-child {
margin-left: 10px;
align-self: flex-end;
}
}
}
}

View File

@@ -0,0 +1,157 @@
/**
* External dependencies
*/
import classnames from 'classnames'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Component } from '@wordpress/element'
import { Button } from '@wordpress/components'
import { RichText, MediaUpload } from '@wordpress/block-editor'
/**
* Internal dependencies
*/
import MediaUploader from '@blocks/shared/MediaUploader'
import { applyFilters } from "@wordpress/hooks";
/**
* A Step within HowTo block.
*/
class Step extends Component {
/**
* Renders the component.
*
* @return {Component} Step editor.
*/
render() {
const {
title,
content,
visible,
imageID,
sizeSlug,
titleWrapper,
titleCssClasses,
contentCssClasses,
} = this.props
const wrapperClasses = classnames( 'rank-math-step-wrapper', {
'step-not-visible': ! visible,
} )
return (
<div className={ wrapperClasses }>
<div className="rank-math-item-header">
<RichText
tagName={ titleWrapper }
className={
'rank-math-howto-step-title rank-math-block-title' +
titleCssClasses
}
value={ title }
onChange={ ( newTitle ) => {
this.setStepProp( 'title', newTitle )
} }
placeholder={ __( 'Enter a step title', 'rank-math' ) }
/>
<div className="rank-math-block-actions">
{ applyFilters( 'rank_math_block_howto_actions', '', this.props ) }
<Button
className="rank-math-item-visbility"
icon={ visible ? 'visibility' : 'hidden' }
onClick={ this.toggleVisibility }
title={ __( 'Hide Step', 'rank-math' ) }
/>
<Button
icon="trash"
className="rank-math-item-delete"
onClick={ this.deleteStep }
title={ __( 'Delete Step', 'rank-math' ) }
/>
</div>
</div>
<MediaUpload
allowedTypes={ [ 'image' ] }
multiple={ false }
value={ imageID }
render={ ( { open } ) => (
<MediaUploader
imageID={ imageID }
sizeSlug={ sizeSlug }
open={ open }
addButtonLabel={ __(
'Add Step Image',
'rank-math'
) }
removeImage={ () => {
this.setStepProp( 'imageID', 0 )
} }
/>
) }
onSelect={ ( image ) => {
this.setStepProp( 'imageID', image.id )
} }
/>
<RichText
tagName="div"
className={
'rank-math-howto-step-content' + contentCssClasses
}
value={ content }
onChange={ ( newContent ) => {
this.setStepProp( 'content', newContent )
} }
placeholder={ __(
'Enter a step description',
'rank-math'
) }
/>
</div>
)
}
/**
* Update step properties.
*
* @param {string} prop Poperty name.
* @param {string} value Property value.
*/
setStepProp( prop, value ) {
const { setAttributes, index } = this.props
const steps = [ ...this.props.steps ]
steps[ index ][ prop ] = value
setAttributes( { steps } )
}
/**
* Toggle step visibility.
*/
toggleVisibility = () => {
const { setAttributes, index } = this.props
const steps = [ ...this.props.steps ]
steps[ index ].visible = ! this.props.visible
setAttributes( { steps } )
}
/**
* Delete step from block.
*/
deleteStep = () => {
const { setAttributes, index } = this.props
const steps = [ ...this.props.steps ]
steps.splice( index, 1 )
setAttributes( { steps } )
}
}
export default Step

View File

@@ -0,0 +1,139 @@
/**
* External dependencies
*/
import { map } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { withSelect } from '@wordpress/data'
import { InspectorControls } from '@wordpress/block-editor'
import { PanelBody, SelectControl, TextControl } from '@wordpress/components'
/**
* Format array of image sizes.
*
* @param {Array} imageSizes Array of image sizes.
* @return {Array} Formatted array.
*/
const getImageSizeOptions = ( imageSizes ) => {
return map( imageSizes, ( { name, slug } ) => ( {
value: slug,
label: name,
} ) )
}
/**
* Adds controls to the editor sidebar to control params.
*
* @param {Object} props This component's props.
*/
const Inspector = ( { imageSizes, attributes, setAttributes } ) => {
const imageSizeOptions = getImageSizeOptions( imageSizes )
return (
<InspectorControls key={ 'inspector' }>
<PanelBody title={ __( 'HowTo Options', 'rank-math' ) }>
<SelectControl
label={ __( 'List Style', 'rank-math' ) }
value={ attributes.listStyle }
options={ [
{
value: '',
label: __( 'None', 'rank-math' ),
},
{
value: 'numbered',
label: __( 'Numbered', 'rank-math' ),
},
{
value: 'unordered',
label: __( 'Unordered', 'rank-math' ),
},
] }
onChange={ ( listStyle ) => {
setAttributes( { listStyle } )
} }
/>
<SelectControl
label={ __( 'Title Wrapper', 'rank-math' ) }
value={ attributes.titleWrapper }
options={ [
{ value: 'h2', label: __( 'H2', 'rank-math' ) },
{ value: 'h3', label: __( 'H3', 'rank-math' ) },
{ value: 'h4', label: __( 'H4', 'rank-math' ) },
{ value: 'h5', label: __( 'H5', 'rank-math' ) },
{ value: 'h6', label: __( 'H6', 'rank-math' ) },
{ value: 'p', label: __( 'P', 'rank-math' ) },
{ value: 'div', label: __( 'DIV', 'rank-math' ) },
] }
onChange={ ( titleWrapper ) => {
setAttributes( { titleWrapper } )
} }
/>
<SelectControl
label={ __( 'Main Image Size', 'rank-math' ) }
value={ attributes.mainSizeSlug }
options={ imageSizeOptions }
onChange={ ( mainSizeSlug ) => {
setAttributes( { mainSizeSlug } )
} }
/>
<SelectControl
label={ __( 'Image Size', 'rank-math' ) }
value={ attributes.sizeSlug }
options={ imageSizeOptions }
onChange={ ( sizeSlug ) => {
setAttributes( { sizeSlug } )
} }
/>
</PanelBody>
<PanelBody title={ __( 'Styling Options', 'rank-math' ) }>
<TextControl
label={ __(
'Step Title Wrapper CSS Class(es)',
'rank-math'
) }
value={ attributes.titleCssClasses }
onChange={ ( titleCssClasses ) => {
setAttributes( { titleCssClasses } )
} }
/>
<TextControl
label={ __(
'Step Content Wrapper CSS Class(es)',
'rank-math'
) }
value={ attributes.contentCssClasses }
onChange={ ( contentCssClasses ) => {
setAttributes( { contentCssClasses } )
} }
/>
<TextControl
label={ __( 'Step List CSS Class(es)', 'rank-math' ) }
value={ attributes.listCssClasses }
onChange={ ( listCssClasses ) => {
setAttributes( { listCssClasses } )
} }
/>
</PanelBody>
</InspectorControls>
)
}
export default withSelect( ( select, props ) => {
const { getSettings } = select( 'core/block-editor' )
const { imageSizes } = getSettings()
return {
...props,
imageSizes,
}
} )( Inspector )

View File

@@ -0,0 +1,290 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Fragment } from '@wordpress/element'
import { applyFilters } from '@wordpress/hooks'
import {
Button,
Dashicon,
TextControl,
ToggleControl,
} from '@wordpress/components'
import {
BlockControls,
AlignmentToolbar,
RichText,
MediaUpload,
useBlockProps,
} from '@wordpress/block-editor'
/**
* Internal dependencies
*/
import Step from './components/Step'
import Inspector from './components/inspector'
import generateId from '@helpers/generateId'
import MediaUploader from '@blocks/shared/MediaUploader'
/**
* Add an empty Step into block.
*
* @param {Object} props Block props.
*/
const addNew = ( props ) => {
const { steps } = props.attributes
const newSteps = isEmpty( steps ) ? [] : [ ...steps ]
newSteps.push( {
id: generateId( 'howto-step' ),
title: '',
content: '',
visible: true,
} )
props.setAttributes( { steps: newSteps } )
}
/**
* Toggle duration form visibility.
*
* @param {Object} props Block props
*/
const toggleDuration = ( props ) => {
props.setAttributes( {
hasDuration: ! props.attributes.hasDuration,
} )
}
/**
* When an image selected.
*
* @param {Object} image Seelected image object.
* @param {Object} props Block props.
*/
const onSelectImage = ( image, props ) => {
const { setAttributes } = props
setAttributes( { imageID: image.id } )
}
/**
* Remove image from step.
*
* @param {Object} props Block props.
*/
const removeImage = ( props ) => {
const { setAttributes } = props
setAttributes( { imageID: 0 } )
}
/**
* Render Steps component.
*
* @param {Object} props Block props.
* @return {Array} Array of step editor.
*/
const renderSteps = ( props ) => {
const {
steps,
sizeSlug,
titleWrapper,
titleCssClasses,
contentCssClasses,
} = props.attributes
if ( isEmpty( steps ) ) {
return null
}
return steps.map( ( step, index ) => {
return (
<li key={ step.id }>
<Step
{ ...step }
index={ index }
key={ step.id + '-step' }
steps={ steps }
setAttributes={ props.setAttributes }
sizeSlug={ sizeSlug }
titleWrapper={ titleWrapper }
titleCssClasses={ titleCssClasses }
contentCssClasses={ contentCssClasses }
/>
</li>
)
} )
}
/**
* HowTo block edit component.
*
* @param {Object} props Block props.
*/
export default ( props ) => {
const { className, isSelected, attributes, setAttributes } = props
const { imageID, mainSizeSlug, textAlign } = attributes
const blockProps = useBlockProps()
return (
<div { ...blockProps }>
<div
id="rank-math-howto"
className={ 'rank-math-block ' + className }
>
{ isSelected && <Inspector { ...props } /> }
{ isSelected && (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ textAlign }
onChange={ ( nextTextAlignment ) =>
props.setAttributes( {
textAlign: nextTextAlignment,
} )
}
/>
</BlockControls>
</Fragment>
) }
<MediaUpload
allowedTypes={ [ 'image' ] }
multiple={ false }
value={ imageID }
render={ ( { open } ) => (
<div className="rank-math-howto-final-image">
<MediaUploader
imageID={ imageID }
sizeSlug={ mainSizeSlug }
open={ open }
addButtonLabel={ __(
'Add Final Image',
'rank-math'
) }
removeImage={ () => {
removeImage( props )
} }
/>
</div>
) }
onSelect={ ( image ) => {
onSelectImage( image, props )
} }
/>
<RichText
style={ { textAlign } }
tagName="div"
className="rank-math-howto-description"
value={ attributes.description }
onChange={ ( description ) => {
setAttributes( { description } )
} }
placeholder={ __(
'Enter a main description',
'rank-math'
) }
/>
<div className={ 'rank-math-howto-duration' }>
<div
className={
'components-base-control rank-math-howto-duration-label'
}
>
<span>{ __( 'Duration', 'rank-math' ) }</span>
<ToggleControl
checked={ attributes.hasDuration }
onChange={ () => {
toggleDuration( props )
} }
/>
</div>
<div
className={
'rank-math-howto-duration-fields' +
( attributes.hasDuration ? '' : ' hidden' )
}
>
<TextControl
value={ attributes.timeLabel }
placeholder={ __( 'Total time:', 'rank-math' ) }
onChange={ ( timeLabel ) => {
setAttributes( { timeLabel } )
} }
/>
<TextControl
type="number"
value={ attributes.days }
placeholder={ __( 'DD', 'rank-math' ) }
onChange={ ( days ) => {
setAttributes( { days } )
} }
/>
<TextControl
type="number"
value={ attributes.hours }
placeholder={ __( 'HH', 'rank-math' ) }
onChange={ ( hours ) => {
setAttributes( { hours } )
} }
/>
<TextControl
type="number"
value={ attributes.minutes }
placeholder={ __( 'MM', 'rank-math' ) }
onChange={ ( minutes ) => {
setAttributes( { minutes } )
} }
/>
</div>
<div
className={
'rank-math-howto-duration-instructions' +
( attributes.hasDuration ? '' : ' hidden' )
}
>
{ __(
'Optional, use first field to describe the duration.',
'rank-math'
) }
</div>
</div>
{ applyFilters( 'rank_math_block_howto_data', '', props ) }
<ul style={ { textAlign } }>{ renderSteps( props ) }</ul>
<Button
variant="primary"
onClick={ () => {
addNew( props )
} }
>
{ __( 'Add New Step', 'rank-math' ) }
</Button>
<a
href="http://rankmath.com/blog/howto-schema/"
title={ __( 'More Info', 'rank-math' ) }
target="_blank"
rel="noopener noreferrer"
className={ 'rank-math-block-info' }
>
<Dashicon icon="info" />
</a>
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
/**
* HowTo eample for in block preview pane.
*
* @type {Object}
*/
export default {
attributes: {
steps: [
{
visible: true,
titleWrapper: 'div',
title: 'Step # 1',
content:
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
},
],
},
}

View File

@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import example from './example'
import edit from './edit'
import save from './save'
import transforms from './transforms'
/**
* Register HowTo block.
*/
registerBlockType(
'rank-math/howto-block',
{
example,
edit,
save,
transforms,
}
)

View File

@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { RichText, useBlockProps } from '@wordpress/block-editor'
/**
* Save block for display on front
*
* @param {Object} props This component's props.
*/
export default ( props ) => {
const { steps, titleWrapper } = props.attributes
if ( isEmpty( steps ) ) {
return null
}
return (
<div { ...useBlockProps.save() }>
{ steps.map( ( step, index ) => {
if ( false === step.visible ) {
return null
}
return (
<div className="rank-math-howto-step" key={ index }>
{ step.title && (
<RichText.Content
tagName={ titleWrapper }
value={ step.title }
className="rank-math-howto-title"
/>
) }
{ step.content && (
<RichText.Content
tagName="div"
value={ step.content }
className="rank-math-howto-content"
/>
) }
</div>
)
} ) }
</div>
)
}

View File

@@ -0,0 +1,47 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import generateId from '@helpers/generateId'
/**
* Transform yoast howto block.
*
* @type {Array}
*/
export default {
from: [
{
type: 'block',
blocks: [ 'yoast/how-to-block' ],
transform: ( yoast ) => {
const steps = yoast.steps.map( ( step ) => {
return {
visible: true,
id: generateId( 'howto-step' ),
title: step.jsonName,
content: step.jsonText,
}
} )
const attributes = {
steps,
titleWrapper: 'h3',
hasDuration: yoast.hasDuration,
days: yoast.days,
hours: yoast.hours,
minutes: yoast.minutes,
description: yoast.jsonDescription,
className: yoast.className,
listStyle: yoast.unorderedList ? 'unordered' : '',
}
return createBlock( 'rank-math/howto-block', attributes )
},
},
],
}

View File

@@ -0,0 +1,89 @@
{
"apiVersion": 3,
"title": "HowTo by Rank Math",
"description": "Easily add Schema-ready, SEO-friendly, HowTo block to your content.",
"name": "rank-math/howto-block",
"category": "rank-math-blocks",
"icon": "editor-ol",
"textdomain": "rank-math",
"keywords": [ "HowTo", "Schema", "SEO", "Structured Data", "Yoast", "Rank Math", "Block", "Markup", "Rich Snippet" ],
"editorScript": [
"lodash",
"file:./assets/js/index.js"
],
"editorStyle": [
"rank-math-block-admin",
"file:./assets/css/howto.css"
],
"attributes": {
"hasDuration": {
"type": "boolean",
"default": false
},
"days": {
"type": "string",
"default": ""
},
"hours": {
"type": "string",
"default": ""
},
"minutes": {
"type": "string",
"default": ""
},
"description": {
"type": "string",
"default": ""
},
"steps": {
"type": "array",
"default": [],
"items": {
"type": "object"
}
},
"sizeSlug": {
"type": "string",
"default": "full"
},
"imageID": {
"type": "integer"
},
"mainSizeSlug": {
"type": "string",
"default": "full"
},
"listStyle": {
"type": "string",
"default": ""
},
"timeLabel": {
"type": "string",
"default": ""
},
"titleWrapper": {
"type": "string",
"default": "h3"
},
"listCssClasses": {
"type": "string",
"default": ""
},
"titleCssClasses": {
"type": "string",
"default": ""
},
"contentCssClasses": {
"type": "string",
"default": ""
},
"textAlign": {
"type": "string",
"default": "left"
}
},
"supports": {
"multiple": false
}
}

View File

@@ -0,0 +1,453 @@
<?php
/**
* The HowTo Block
*
* @since 1.0.233
* @package RankMath
* @subpackage RankMath\HowTo
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
use RankMath\Traits\Hooker;
use RankMath\Paper\Paper;
use RankMath\Helpers\Attachment;
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* HowTo Block class.
*/
class Block_HowTo extends Block {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/howto-block';
/**
* The single instance of the class.
*
* @var Block_HowTo
*/
protected static $instance = null;
/**
* Retrieve main Block_HowTo instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_HowTo
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_HowTo ) ) {
self::$instance = new Block_HowTo();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type(
RANK_MATH_PATH . 'includes/modules/schema/blocks/howto/block.json',
[
'render_callback' => [ $this, 'render' ],
]
);
add_filter( 'rank_math/schema/block/howto-block', [ $this, 'add_graph' ], 10, 2 );
}
/**
* Add HowTO schema data in JSON-LD array..
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
// Early bail.
if ( ! $this->has_steps( $block['attrs'] ) ) {
return $data;
}
$attrs = $block['attrs'];
if ( ! isset( $data['howto'] ) ) {
$data['howto'] = [
'@type' => 'HowTo',
'name' => Paper::get()->get_title(),
'description' => isset( $attrs['description'] ) ? $this->clean_text( do_shortcode( $attrs['description'] ) ) : '',
'totalTime' => '',
'step' => [],
];
}
$this->add_step_image( $data['howto'], $attrs );
$this->add_duration( $data['howto'], $attrs );
$permalink = get_permalink() . '#';
foreach ( $attrs['steps'] as $index => $step ) {
if ( empty( $step['visible'] ) ) {
continue;
}
$schema_step = $this->add_step( $step, $permalink . $step['id'] );
if ( $schema_step ) {
$data['howto']['step'][] = $schema_step;
}
}
return $data;
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
* @return string
*/
public static function markup( $attributes = [] ) {
$list_style = isset( $attributes['listStyle'] ) ? esc_attr( $attributes['listStyle'] ) : '';
$list_css_classes = isset( $attributes['listCssClasses'] ) ? esc_attr( $attributes['listCssClasses'] ) : '';
$title_wrapper = isset( $attributes['titleWrapper'] ) ? esc_attr( $attributes['titleWrapper'] ) : 'h3';
$title_css_classes = isset( $attributes['titleCssClasses'] ) ? esc_attr( $attributes['titleCssClasses'] ) : '';
$content_css_classes = isset( $attributes['contentCssClasses'] ) ? esc_attr( $attributes['contentCssClasses'] ) : '';
$size_slug = isset( $attributes['sizeSlug'] ) ? esc_attr( $attributes['sizeSlug'] ) : '';
$list_tag = self::get()->get_list_style( $list_style );
$item_tag = self::get()->get_list_item_style( $list_style );
$class = 'rank-math-block';
if ( ! empty( $attributes['className'] ) ) {
$class .= ' ' . esc_attr( $attributes['className'] );
}
// HTML.
$out = [];
$out[] = sprintf( '<div id="rank-math-howto" class="%1$s" %2$s>', esc_attr( $class ), self::get()->get_styles( $attributes ) );
// HeaderContent.
$out[] = '<div class="rank-math-howto-description">';
if ( ! empty( $attributes['imageUrl'] ) ) {
$out[] = '<img src="' . esc_url( $attributes['imageUrl'] ) . '" />';
} elseif ( ! empty( $attributes['mainSizeSlug'] ) ) {
$out[] = self::get()->get_image( $attributes, $attributes['mainSizeSlug'], '' );
}
if ( ! empty( $attributes['description'] ) ) {
$out[] = self::get()->normalize_text( $attributes['description'], 'howto' );
}
$out[] = '</div>';
$out[] = self::get()->build_duration( $attributes );
$out[] = sprintf( '<%1$s class="rank-math-steps %2$s">', $list_tag, $list_css_classes );
// Steps.
foreach ( $attributes['steps'] as $index => $step ) {
if ( empty( $step['visible'] ) ) {
continue;
}
$step_id = isset( $step['id'] ) ? esc_attr( $step['id'] ) : '';
$out[] = sprintf( '<%1$s id="%2$s" class="rank-math-step">', $item_tag, $step_id );
if ( ! empty( $step['title'] ) ) {
$out[] = sprintf(
'<%1$s class="rank-math-step-title %2$s">%3$s</%1$s>',
self::get()->get_title_wrapper( $title_wrapper, 'howto' ),
$title_css_classes,
wp_kses_post( $step['title'] )
);
}
$step_content = ! empty( $step['content'] ) ? self::get()->normalize_text( $step['content'], 'howto' ) : '';
$step_image = ! empty( $step['imageUrl'] ) ? '<img src="' . esc_url( $step['imageUrl'] ) . '" />' : self::get()->get_image( $step, $size_slug, '' );
$out[] = sprintf(
'<div class="rank-math-step-content %1$s">%3$s%2$s</div>',
$content_css_classes,
$step_content,
$step_image
);
$out[] = sprintf( '</%1$s>', $item_tag );
}
$out[] = sprintf( '</%1$s>', $list_tag );
$out[] = '</div>';
return apply_filters(
'rank_math/schema/block/howto/content',
wp_kses_post( join( "\n", $out ) ),
$out,
$attributes
);
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
*
* @return string
*/
public function render( $attributes ) {
// Early bail.
if ( ! $this->has_steps( $attributes ) ) {
return '';
}
return self::markup( $attributes );
}
/**
* Add Step
*
* @param array $step Step.
* @param string $permalink Permalink.
*/
private function add_step( $step, $permalink ) {
$name = wp_strip_all_tags( do_shortcode( $step['title'] ) );
$text = $this->clean_text( do_shortcode( $step['content'] ) );
if ( empty( $name ) && empty( $text ) ) {
return false;
}
$schema_step = [
'@type' => 'HowToStep',
'url' => '' . esc_url( $permalink ),
];
if ( empty( $name ) ) {
$schema_step['text'] = '';
if ( empty( $text ) && empty( $schema_step['image'] ) ) {
return false;
}
if ( ! empty( $text ) ) {
$schema_step['text'] = $text;
}
} elseif ( empty( $text ) ) {
$schema_step['text'] = $name;
} else {
$schema_step['name'] = $name;
if ( ! empty( $text ) ) {
$schema_step['itemListElement'] = [
[
'@type' => 'HowToDirection',
'text' => $text,
],
];
}
}
if ( false === $this->add_step_image( $schema_step, $step ) ) {
$this->add_step_image_from_content( $schema_step, $step );
}
return $schema_step;
}
/**
* Checks if we have an inline image and add it.
*
* @param array $schema_step Our Schema output for the Step.
* @param array $step The step block data.
*/
private function add_step_image_from_content( &$schema_step, $step ) {
// Early Bail.
if ( empty( $step['content'] ) || ! Str::contains( '<img', $step['content'] ) ) {
return;
}
// Search for image.
preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"].*?>/i', $step['content'], $matches );
if ( ! isset( $matches[1][0] ) || empty( $matches[1][0] ) ) {
return;
}
$schema_image = [
'@type' => 'ImageObject',
'url' => esc_url( $matches[1][0] ),
];
$image_id = Attachment::get_by_url( $schema_image['url'] );
if ( $image_id > 0 ) {
$this->add_caption( $schema_image, $image_id );
$this->add_image_size( $schema_image, $image_id );
}
$schema_step['image'] = $schema_image;
}
/**
* Checks if we have a step image and add it.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*
* @param array $schema_step Our Schema output for the Step.
* @param array $step The step block data.
*/
private function add_step_image( &$schema_step, $step ) {
if ( empty( $step['imageID'] ) ) {
return false;
}
$image_id = absint( $step['imageID'] );
if ( ! ( $image_id > 0 ) ) {
return false;
}
$image_url = wp_get_attachment_image_url( $image_id, 'full' );
if ( ! $image_url ) {
return false;
}
$schema_image = [
'@type' => 'ImageObject',
'url' => esc_url( $image_url ),
];
$this->add_caption( $schema_image, $image_id );
$this->add_image_size( $schema_image, $image_id );
$schema_step['image'] = $schema_image;
return true;
}
/**
* Add caption to schema.
*
* @param array $schema_image Our Schema output for the Image.
* @param int $image_id The image ID.
*/
private function add_caption( &$schema_image, $image_id ) {
$caption = wp_get_attachment_caption( $image_id );
if ( ! empty( $caption ) ) {
$schema_image['caption'] = esc_html( $caption );
return;
}
$caption = Attachment::get_alt_tag( $image_id );
if ( ! empty( $caption ) ) {
$schema_image['caption'] = esc_html( $caption );
}
}
/**
* Add image size to schema.
*
* @param array $schema_image Our Schema output for the Image.
* @param int $image_id The image ID.
*/
private function add_image_size( &$schema_image, $image_id ) {
$image_meta = wp_get_attachment_metadata( $image_id );
if ( empty( $image_meta['width'] ) || empty( $image_meta['height'] ) ) {
return;
}
$schema_image['width'] = absint( $image_meta['width'] );
$schema_image['height'] = absint( $image_meta['height'] );
}
/**
* Add duration to schema.
*
* @param array $data Our Schema output.
* @param array $attrs The block attributes.
*/
private function add_duration( &$data, $attrs ) {
if ( ! empty( $attrs['hasDuration'] ) ) {
$days = absint( $attrs['days'] ?? 0 );
$hours = absint( $attrs['hours'] ?? 0 );
$minutes = absint( $attrs['minutes'] ?? 0 );
if ( ( $days + $hours + $minutes ) > 0 ) {
$data['totalTime'] = esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' );
}
}
}
/**
* Generate HowTo duration property.
*
* @param array $attrs The block attributes.
*
* @return string
*/
private function build_duration( $attrs ) {
if ( empty( $attrs['hasDuration'] ) ) {
return '';
}
$days = isset( $attrs['days'] ) ? absint( $attrs['days'] ) : 0;
$hours = isset( $attrs['hours'] ) ? absint( $attrs['hours'] ) : 0;
$minutes = isset( $attrs['minutes'] ) ? absint( $attrs['minutes'] ) : 0;
$elements = [];
if ( $days > 0 ) {
/* translators: %d is the number of days. */
$elements[] = sprintf( _n( '%d day', '%d days', $days, 'rank-math' ), $days );
}
if ( $hours > 0 ) {
/* translators: %d is the number of hours. */
$elements[] = sprintf( _n( '%d hour', '%d hours', $hours, 'rank-math' ), $hours );
}
if ( $minutes > 0 ) {
/* translators: %d is the number of minutes. */
$elements[] = sprintf( _n( '%d minute', '%d minutes', $minutes, 'rank-math' ), $minutes );
}
$count = count( $elements );
$formats = [
1 => '%1$s',
/* translators: placeholders are units of time, e.g. '1 hour and 30 minutes' */
2 => __( '%1$s and %2$s', 'rank-math' ),
/* translators: placeholders are units of time, e.g. '1 day, 8 hours and 30 minutes' */
3 => __( '%1$s, %2$s and %3$s', 'rank-math' ),
];
return sprintf(
'<p class="rank-math-howto-duration"><strong>%2$s</strong> <span>%1$s</span></p>',
isset( $formats[ $count ] ) ? vsprintf( $formats[ $count ], $elements ) : '',
empty( $attrs['timeLabel'] ) ? __( 'Total Time:', 'rank-math' ) : esc_html( $attrs['timeLabel'] )
);
}
/**
* Function to check the HowTo block have steps data.
*
* @param array $attributes Array of attributes.
*
* @return boolean
*/
private function has_steps( $attributes ) {
return ! isset( $attributes['steps'] ) || empty( $attributes['steps'] ) ? false : true;
}
}

View File

@@ -0,0 +1,5 @@
/*!
* Plugin: Rank Math
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: rank-math-review-snippet.css
*/@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(-360deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(-360deg)}}@keyframes bounce{from{-webkit-transform:translateY(0px);transform:translateY(0px)}to{-webkit-transform:translateY(-5px);transform:translateY(-5px)}}@-webkit-keyframes bounce{from{-webkit-transform:translateY(0px);transform:translateY(0px)}to{-webkit-transform:translateY(-5px);transform:translateY(-5px)}}@-webkit-keyframes loading{0%{background-size:20% 50%,20% 50%,20% 50%}20%{background-size:20% 20%,20% 50%,20% 50%}40%{background-size:20% 100%,20% 20%,20% 50%}60%{background-size:20% 50%,20% 100%,20% 20%}80%{background-size:20% 50%,20% 50%,20% 100%}100%{background-size:20% 50%,20% 50%,20% 50%}}@keyframes loading{0%{background-size:20% 50%,20% 50%,20% 50%}20%{background-size:20% 20%,20% 50%,20% 50%}40%{background-size:20% 100%,20% 20%,20% 50%}60%{background-size:20% 50%,20% 100%,20% 20%}80%{background-size:20% 50%,20% 50%,20% 100%}100%{background-size:20% 50%,20% 50%,20% 50%}}:root{--rankmath-wp-adminbar-height: 0}#rank-math-rich-snippet-wrapper{overflow:hidden}#rank-math-rich-snippet-wrapper h5.rank-math-title{display:block;font-size:18px;line-height:1.4}#rank-math-rich-snippet-wrapper .rank-math-review-image{float:right;max-width:40%;margin-left:15px}#rank-math-rich-snippet-wrapper .rank-math-review-data{margin-bottom:15px}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper{width:100%;padding:0 0 20px 0;float:left;clear:both;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-total{border:0;display:block;margin:0;width:auto;float:left;text-align:left;padding:0;font-size:24px;line-height:1;font-weight:700;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star{float:left;margin-left:15px;margin-top:5px;position:relative;z-index:99;line-height:1}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star .rank-math-review-result-wrapper{display:inline-block;white-space:nowrap;position:relative;color:#e7e7e7}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star .rank-math-review-result-wrapper .rank-math-review-result{position:absolute;top:0;left:0;overflow:hidden;white-space:nowrap;color:#ffbe01}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star .rank-math-review-result-wrapper i{font-size:18px;-webkit-text-stroke-width:1px;font-style:normal;padding:0 2px;line-height:inherit}#rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star .rank-math-review-result-wrapper i:before{content:"★"}body.rtl #rank-math-rich-snippet-wrapper .rank-math-review-image{float:left;margin-left:0;margin-right:15px}body.rtl #rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-total{float:right}body.rtl #rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star{float:right;margin-left:0;margin-right:15px}body.rtl #rank-math-rich-snippet-wrapper .rank-math-total-wrapper .rank-math-review-star .rank-math-review-result{left:auto;right:0}@media screen and (max-width: 480px){#rank-math-rich-snippet-wrapper .rank-math-review-image{display:block;max-width:100%;width:100%;text-align:center;margin-right:0}#rank-math-rich-snippet-wrapper .rank-math-review-data{clear:both}}.clear{clear:both}

View File

@@ -0,0 +1 @@
!function(){"use strict";var e={n:function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,{a:r}),r},d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t=lodash,r=wp.blocks,n=wp.i18n,o=wp.blockEditor,l=wp.components;function a(){return a=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e},a.apply(this,arguments)}var s=wp.element,c=wp.data,i=wp.hooks;const u=Object.create(null);var p=wp.compose,d=wp.apiFetch,m=e.n(d),f=wp.url;const h={};function w(e){let{className:t}=e;return(0,s.createElement)(l.Placeholder,{className:t},(0,n.__)("Block rendered as empty."))}function v(e){let{response:t,className:r}=e;const o=(0,n.sprintf)((0,n.__)("Error loading block: %s"),t.errorMsg);return(0,s.createElement)(l.Placeholder,{className:r},o)}function b(e){let{children:t,showLoader:r}=e;return(0,s.createElement)("div",{style:{position:"relative"}},r&&(0,s.createElement)("div",{style:{position:"absolute",top:"50%",left:"50%",marginTop:"-9px",marginLeft:"-9px"}},(0,s.createElement)(l.Spinner,null)),(0,s.createElement)("div",{style:{opacity:r?"0.3":1}},t))}function E(e){const{attributes:n,block:o,className:l,httpMethod:c="GET",urlQueryArgs:i,skipBlockSupportAttributes:u=!1,EmptyResponsePlaceholder:d=w,ErrorResponsePlaceholder:E=v,LoadingResponsePlaceholder:y=b}=e,g=(0,s.useRef)(!0),[k,_]=(0,s.useState)(!1),P=(0,s.useRef)(),[S,$]=(0,s.useState)(null),T=(0,p.usePrevious)(e),[C,R]=(0,s.useState)(!1);function M(){var e,t;if(!g.current)return;R(!0);let l=n&&(0,r.__experimentalSanitizeBlockAttributes)(o,n);u&&(l=function(e){const{backgroundColor:t,borderColor:r,fontFamily:n,fontSize:o,gradient:l,textColor:a,className:s,...c}=e,{border:i,color:u,elements:p,spacing:d,typography:m,...f}=(null==e?void 0:e.style)||h;return{...c,style:f}}(l));const a="POST"===c,s=a?null:null!==(e=l)&&void 0!==e?e:null,p=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return(0,f.addQueryArgs)(`/wp/v2/block-renderer/${e}`,{context:"edit",...null!==t?{attributes:t}:{},...r})}(o,s,i),d=a?{attributes:null!==(t=l)&&void 0!==t?t:null}:null,w=P.current=m()({path:p,data:d,method:a?"POST":"GET"}).then((e=>{g.current&&w===P.current&&e&&$(e.rendered)})).catch((e=>{g.current&&w===P.current&&$({error:!0,errorMsg:e.message})})).finally((()=>{g.current&&w===P.current&&R(!1)}));return w}const N=(0,p.useDebounce)(M,500);(0,s.useEffect)((()=>()=>{g.current=!1}),[]),(0,s.useEffect)((()=>{void 0===T?M():(0,t.isEqual)(T,e)||N()})),(0,s.useEffect)((()=>{if(!C)return;const e=setTimeout((()=>{_(!0)}),1e3);return()=>clearTimeout(e)}),[C]);const O=!!S,j=""===S,A=null==S?void 0:S.error;return C?(0,s.createElement)(y,a({},e,{showLoader:k}),O&&(0,s.createElement)(s.RawHTML,{className:l},S)):j||!O?(0,s.createElement)(d,e):A?(0,s.createElement)(E,a({response:S},e)):(0,s.createElement)(s.RawHTML,{className:l},S)}const y={},g=(0,c.withSelect)((e=>{const t=e("core/editor");if(t){const e=t.getCurrentPostId();if(e&&"number"==typeof e)return{currentPostId:e}}return y}))((e=>{let{urlQueryArgs:t=y,currentPostId:r,...n}=e;const o=(0,s.useMemo)((()=>r?{post_id:r,...t}:t),[r,t]);return(0,s.createElement)(E,a({urlQueryArgs:o},n))}));window&&window.wp&&window.wp.components&&(window.wp.components.ServerSideRender=(0,s.forwardRef)(((e,t)=>(function(e,t={}){const{since:r,version:n,alternative:o,plugin:l,link:a,hint:s}=t,c=`${e} is deprecated${r?` since version ${r}`:""}${n?` and will be removed${l?` from ${l}`:""} in version ${n}`:""}.${o?` Please use ${o} instead.`:""}${a?` See: ${a}`:""}${s?` Note: ${s}`:""}`;c in u||((0,i.doAction)("deprecated",e,t,c),console.warn(c),u[c]=!0)}("wp.components.ServerSideRender",{version:"6.2",since:"5.3",alternative:"wp.serverSideRender"}),(0,s.createElement)(g,a({},e,{ref:t}))))));var k=g;(0,r.registerBlockType)("rank-math/rich-snippet",{edit:function(e){var r=e.attributes,a=e.setAttributes,s=(0,o.useBlockProps)(),c=[];return r.post_id||(r.post_id=rankMath.objectID,a({post_id:rankMath.objectID})),(0,t.forEach)(r,(function(e,o){"post_id"!==o?"className"!==o&&c.push(wp.element.createElement(l.TextControl,{key:o,label:(0,n.__)((0,t.startCase)(o),"rank-math"),value:r[o],type:"string",onChange:function(e){var t={};t[o]=e,a(t)}})):c.push(wp.element.createElement(l.TextControl,{key:o,label:(0,n.__)((0,t.startCase)(o),"rank-math"),value:r[o],type:"number",min:1,step:1,onChange:function(e){var t={};t[o]=e||rankMath.objectID,a(t)}}))})),wp.element.createElement("div",s,wp.element.createElement(o.InspectorControls,null,wp.element.createElement(l.PanelBody,{title:(0,n.__)("Settings","rank-math")},c)),wp.element.createElement(k,{block:"rank-math/rich-snippet",attributes:r}))}})}();

View File

@@ -0,0 +1,143 @@
// compileCompressed: ../css/$1.css
/*!
* Plugin: Rank Math
* URL: https://rankmath.com/wordpress/plugin/seo-suite/
* Name: rank-math-review-snippet.css
*/
// Common and Vendors
@import '../../../../../../../assets/vendor/bourbon/bourbon';
@import '../../../../../../../assets/admin/scss/mixins';
@import '../../../../../../../assets/admin/scss/variables';
/**
* Styling for Rank Math [rank_math_review_snippet] shortcode
*/
#rank-math-rich-snippet-wrapper {
overflow: hidden;
h5.rank-math-title {
display: block;
font-size: 18px;
line-height: 1.4;
}
.rank-math-review-image {
float: right;
max-width: 40%;
margin-left: 15px;
}
.rank-math-review-data {
margin-bottom: 15px;
}
.rank-math-total-wrapper {
width: 100%;
padding: 0 0 20px 0;
float: left;
clear: both;
position: relative;
box-sizing: border-box;
.rank-math-total {
border: 0;
display: block;
margin: 0;
width: auto;
float: left;
text-align: left;
padding: 0;
font-size: 24px;
line-height: 1;
font-weight: 700;
box-sizing: border-box;
overflow: hidden;
}
.rank-math-review-star {
float: left;
margin-left: 15px;
margin-top: 5px;
position: relative;
z-index: 99;
line-height: 1;
.rank-math-review-result-wrapper {
display: inline-block;
white-space: nowrap;
position: relative;
color: #e7e7e7;
.rank-math-review-result {
position: absolute;
top: 0;
left: 0;
overflow: hidden;
white-space: nowrap;
color:#ffbe01;
}
i {
font-size: 18px;
-webkit-text-stroke-width: 1px;
font-style: normal;
padding: 0 2px;
line-height: inherit;
&:before {
content: "\2605";
}
}
}
}
}
}
body.rtl {
#rank-math-rich-snippet-wrapper {
.rank-math-review-image {
float: left;
margin-left: 0;
margin-right: 15px;
}
.rank-math-total-wrapper {
.rank-math-total {
float: right;
}
.rank-math-review-star {
float: right;
margin-left: 0;
margin-right: 15px;
.rank-math-review-result {
left: auto;
right: 0;
}
}
}
}
}
@media screen and (max-width: 480px) {
#rank-math-rich-snippet-wrapper {
.rank-math-review-image {
display: block;
max-width: 100%;
width: 100%;
text-align: center;
margin-right: 0;
}
.rank-math-review-data {
clear: both;
}
}
}
.clear {
clear: both;
}

View File

@@ -0,0 +1,76 @@
/**
* External dependencies
*/
import { startCase, forEach } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { useBlockProps, InspectorControls } from '@wordpress/block-editor'
import { TextControl, PanelBody } from '@wordpress/components'
import ServerSideRender from '@wordpress/server-side-render'
export default ( {
attributes,
setAttributes,
} ) => {
const blockProps = useBlockProps()
const controllers = []
if ( ! attributes.post_id ) {
attributes.post_id = rankMath.objectID
setAttributes( { post_id: rankMath.objectID } )
}
forEach( attributes, ( attribute, slug ) => {
if ( 'post_id' === slug ) {
controllers.push(
<TextControl
key={ slug }
label={ __( startCase( slug ), 'rank-math' ) }
value={ attributes[ slug ] }
type="number"
min={ 1 }
step={ 1 }
onChange={ ( newID ) => {
const attrs = {}
attrs[ slug ] = newID ? newID : rankMath.objectID
setAttributes( attrs )
} }
/>
)
return
}
if ( 'className' !== slug ) {
controllers.push(
<TextControl
key={ slug }
label={ __( startCase( slug ), 'rank-math' ) }
value={ attributes[ slug ] }
type="string"
onChange={ ( nextID ) => {
const attrs = {}
attrs[ slug ] = nextID
setAttributes( attrs )
} }
/>
)
}
} )
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'rank-math' ) }>
{ controllers }
</PanelBody>
</InspectorControls>
<ServerSideRender
block="rank-math/rich-snippet"
attributes={ attributes }
/>
</div>
)
}

View File

@@ -0,0 +1,24 @@
/**
* External Dependencies
*/
import { isUndefined } from 'lodash'
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import edit from './edit'
/**
* Register Schema block.
*/
registerBlockType(
'rank-math/rich-snippet',
{
edit,
}
)

View File

@@ -0,0 +1,18 @@
{
"apiVersion": 3,
"title": "Schema by Rank Math",
"description": "Add the Schema generated by Rank Math anywhere on your page using this Block.",
"name": "rank-math/rich-snippet",
"category": "rank-math-blocks",
"icon": "editor-ul",
"textdomain": "rank-math",
"keywords": [ "Schema", "Markup", "Structured Data", "Rich Snippet", "SEO", "Rank Math", "Yoast" ],
"editorScript": "file:./assets/js/index.js",
"editorStyle": "file:./assets/css/schema.css",
"attributes": {
"post_id": {
"type": "string",
"default": ""
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* The Schema Block
*
* @since 1.0.233
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Schema Block class.
*/
class Block_Schema {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/rich-snippet';
/**
* The single instance of the class.
*
* @var Block_Schema
*/
protected static $instance = null;
/**
* Retrieve main Block_TOC instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_Schema
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_Schema ) ) {
self::$instance = new Block_Schema();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type(
RANK_MATH_PATH . 'includes/modules/schema/blocks/schema/block.json',
[
'render_callback' => [ $this, 'rich_snippet' ],
]
);
}
/**
* Schema Block render callback.
*
* @param array $attributes Block Attributes.
*/
public function rich_snippet( $attributes ) {
$output = '';
foreach ( $attributes as $key => $value ) {
$output .= $key . '="' . esc_attr( $value ) . '" ';
}
return do_shortcode( '[rank_math_rich_snippet ' . trim( $output ) . ']' );
}
}

View File

@@ -0,0 +1,30 @@
/**
* Internal dependencies
*/
import { getImageByID } from '@helpers/imageHelper'
/**
* WordPress dependencies
*/
import { withSelect } from '@wordpress/data'
/**
* Render image from image id.
*
* @param {string} imageUrl Image url.
*/
const ImagePreview = ( { imageUrl } ) => {
if ( ! imageUrl ) {
return null
}
return <img src={ imageUrl } alt="" />
}
export default withSelect( ( select, props ) => {
const { imageID, sizeSlug } = props
return {
imageUrl: imageID ? getImageByID( imageID, sizeSlug ) : null,
}
} )( ImagePreview )

View File

@@ -0,0 +1,55 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Button } from '@wordpress/components'
/**
* Internal dependencies
*/
import ImagePreview from '@blocks/shared/ImagePreview'
/**
* Media uploader component.
*
* @param {Object} props This component's props.
*/
const MediaUploader = ( {
imageID,
sizeSlug,
open,
removeImage,
addButtonLabel = __( 'Add Image', 'rank-math' ),
} ) => {
return (
<div className="rank-math-media-placeholder">
{ imageID > 0 && (
<ImagePreview imageID={ imageID } sizeSlug={ sizeSlug } />
) }
{ imageID > 0 ? (
<Button
icon="edit"
className="rank-math-replace-image"
onClick={ open }
/>
) : (
<Button
onClick={ open }
className="rank-math-add-image"
isPrimary
>
{ addButtonLabel }
</Button>
) }
{ imageID > 0 && (
<Button
icon="no-alt"
className="rank-math-delete-image"
onClick={ removeImage }
/>
) }
</div>
)
}
export default MediaUploader

View File

@@ -0,0 +1 @@
.wp-block-rank-math-toc-block nav li,.wp-block-rank-math-toc-block nav div{position:relative;min-height:28px;margin-bottom:0}.wp-block-rank-math-toc-block nav li.disabled,.wp-block-rank-math-toc-block nav div.disabled{display:block !important;opacity:.5}.wp-block-rank-math-toc-block nav li .components-base-control,.wp-block-rank-math-toc-block nav div .components-base-control{position:absolute;top:2px;left:-4px;right:-3px}.wp-block-rank-math-toc-block nav li .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .rank-math-block-actions{position:absolute;top:1px;right:0;display:none;line-height:1}.wp-block-rank-math-toc-block nav li .rank-math-block-actions button.components-button,.wp-block-rank-math-toc-block nav div .rank-math-block-actions button.components-button{min-width:24px;width:24px;height:24px;line-height:34px}.wp-block-rank-math-toc-block nav li:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav li:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav li .components-base-control+.rank-math-block-actions .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .components-base-control+.rank-math-block-actions .rank-math-block-actions{display:block}.rank-math-toc-exclude-headings{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.rank-math-toc-exclude-headings>div{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;margin-bottom:10px !important}

View File

@@ -0,0 +1 @@
.wp-block-rank-math-toc-block nav ol{counter-reset:item}.wp-block-rank-math-toc-block nav ol li{display:block}.wp-block-rank-math-toc-block nav ol li:before{content:counters(item, ".") ". ";counter-increment:item}

View File

@@ -0,0 +1,25 @@
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase -- This filename format is required to dynamically load the necessary block dependencies.
/**
* Block script dependencies.
*
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
return [
'dependencies' => [
'wp-blocks',
'wp-element',
'wp-components',
'wp-block-editor',
'wp-data',
'wp-dom',
'wp-url',
'wp-i18n',
'lodash',
'wp-primitives',
'wp-reusable-blocks',
],
'version' => rank_math()->version,
];

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
.wp-block-rank-math-toc-block {
nav {
li, div {
position: relative;
min-height: 28px;
margin-bottom: 0;
&.disabled {
display: block !important;
opacity: 0.5;
}
.components-base-control {
position: absolute;
top: 2px;
left: -4px;
right: -3px;
}
.rank-math-block-actions {
position: absolute;
top: 1px;
right: 0;
display: none;
line-height: 1;
button.components-button {
min-width: 24px;
width: 24px;
height: 24px;
line-height: 34px;
}
}
&:hover, &:focus, .components-base-control + .rank-math-block-actions {
.rank-math-block-actions {
display: block;
}
}
}
}
}
.rank-math-toc-exclude-headings {
display: flex;
flex-wrap: wrap;
> div {
flex: 0 0 50%;
margin-bottom: 10px!important;
}
}

View File

@@ -0,0 +1,17 @@
.wp-block-rank-math-toc-block {
nav {
ol {
counter-reset: item;
li {
display: block;
}
li:before {
content: counters(item, ".") ". ";
counter-increment: item;
}
}
}
}

View File

@@ -0,0 +1,55 @@
import { linearToNestedHeadingList } from './utils'
import { useBlockProps } from '@wordpress/block-editor'
import List from './list'
const attributes = {
title: {
type: 'text',
},
headings: {
type: 'array',
items: {
type: 'object',
},
},
listStyle: {
type: 'text',
},
titleWrapper: {
type: 'text',
default: 'h2',
},
excludeHeadings: {
type: 'array',
},
}
const v1 = {
attributes,
save( { attributes } ) {
if ( attributes.headings.length === 0 ) {
return null
}
const TitleWrapper = attributes.titleWrapper
const headings = linearToNestedHeadingList( attributes.headings )
const ListStyle = attributes.listStyle
return (
<div { ...useBlockProps.save() }>
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
<nav>
<ListStyle>
<List
headings={ headings }
ListStyle={ ListStyle }
isSave={ true }
/>
</ListStyle>
</nav>
</div>
)
},
}
export default [ v1 ]

View File

@@ -0,0 +1,138 @@
/**
* External dependencies
*/
import { isUndefined, map, includes, remove } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import {
useBlockProps,
RichText,
store as blockEditorStore,
} from '@wordpress/block-editor'
import { useDispatch } from '@wordpress/data'
import { Placeholder } from '@wordpress/components'
import { useEffect, useState } from '@wordpress/element'
/**
* Internal dependencies
*/
import { GetLatestHeadings, linearToNestedHeadingList } from './utils'
import List from './list'
import InspectControls from './inspectControls'
import Toolbar from './toolbar'
export default ( {
attributes,
setAttributes,
} ) => {
const blockProps = useBlockProps()
// State to monitor edit heading links.
const [ edit, toggleEdit ] = useState( false )
const [ excludeHeading, toggleExcludeHeading ] = useState( {} )
if ( ! attributes.listStyle ) {
setAttributes( { listStyle: rankMath.listStyle } )
}
const ListStyle = attributes.listStyle
const tocTitle = attributes.title ?? rankMath.tocTitle
const excludeHeadings = ! isUndefined( attributes.excludeHeadings ) ? attributes.excludeHeadings : rankMath.tocExcludeHeadings
// Function to hide certain heading.
const hideHeading = ( value, key ) => {
const headings = map( attributes.headings, ( heading ) => {
if ( heading.key === key ) {
heading.disable = value
}
return heading
} )
setAttributes( { headings } )
}
// Function to update Heading link.
const onHeadingUpdate = ( value, key, isContent = false ) => {
const headings = map( attributes.headings, ( heading ) => {
if ( heading.key === key ) {
if ( isContent ) {
heading.content = value
heading.isUpdated = true
} else {
heading.isGeneratedLink = false
heading.link = value
}
}
return heading
} )
setAttributes( { headings } )
}
const setExcludeHeadings = ( headingLevel ) => {
if ( includes( excludeHeadings, headingLevel ) ) {
remove( excludeHeadings, ( heading ) => {
return heading === headingLevel
} )
} else {
excludeHeadings.push( headingLevel )
}
setAttributes( { excludeHeadings } )
toggleExcludeHeading( ! excludeHeading )
}
const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore )
// Get Latest headings from the content.
const latestHeadings = GetLatestHeadings( attributes.headings, excludeHeadings )
useEffect( () => {
if ( latestHeadings !== null ) {
__unstableMarkNextChangeAsNotPersistent()
setAttributes( { headings: latestHeadings } )
}
}, [ latestHeadings ] )
const headingTree = linearToNestedHeadingList( attributes.headings )
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
return (
<div { ...blockProps }>
<Placeholder
label={ __( 'Table of Contents', 'rank-math' ) }
instructions={ __( 'Add Heading blocks to this page to generate the Table of Contents.', 'rank-math' ) }
/>
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
</div>
)
}
return (
<div { ...blockProps }>
<RichText
tagName={ attributes.titleWrapper }
value={ tocTitle }
onChange={ ( newTitle ) => {
setAttributes( { title: newTitle } )
} }
placeholder={ __( 'Enter a title', 'rank-math' ) }
/>
<nav>
<ListStyle>
<List
headings={ headingTree }
onHeadingUpdate={ onHeadingUpdate }
edit={ edit }
toggleEdit={ toggleEdit }
hideHeading={ hideHeading }
ListStyle={ ListStyle }
/>
</ListStyle>
</nav>
<Toolbar setAttributes={ setAttributes } />
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
</div>
)
}

View File

@@ -0,0 +1,23 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import edit from './edit'
import save from './save'
import deprecated from './deprecated'
/**
* Register TOC block
*/
registerBlockType(
'rank-math/toc-block',
{
edit,
save,
deprecated,
}
)

View File

@@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { map, includes, toUpper } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { InspectorControls } from '@wordpress/block-editor'
import {
PanelBody,
SelectControl,
CheckboxControl,
} from '@wordpress/components'
export default ( { attributes, setAttributes, excludeHeadings, setExcludeHeadings } ) => {
return (
<InspectorControls>
<PanelBody title={ __( 'Settings', 'rank-math' ) }>
<SelectControl
label={ __( 'Title Wrapper', 'rank-math' ) }
value={ attributes.titleWrapper }
options={ [
{ value: 'h2', label: __( 'H2', 'rank-math' ) },
{ value: 'h3', label: __( 'H3', 'rank-math' ) },
{ value: 'h4', label: __( 'H4', 'rank-math' ) },
{ value: 'h5', label: __( 'H5', 'rank-math' ) },
{ value: 'h6', label: __( 'H6', 'rank-math' ) },
{ value: 'p', label: __( 'P', 'rank-math' ) },
{ value: 'div', label: __( 'DIV', 'rank-math' ) },
] }
onChange={ ( titleWrapper ) => {
setAttributes( { titleWrapper } )
} }
/>
<br />
<h3>{ __( 'Exclude Headings', 'rank-math' ) }</h3>
<div className="rank-math-toc-exclude-headings">
{
map( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], ( value ) => {
return (
<CheckboxControl
key={ value }
label={ __( 'Heading ', 'rank-math' ) + toUpper( value ) }
checked={ includes( excludeHeadings, value ) }
onChange={ ( newVlaue ) => setExcludeHeadings( value, newVlaue ) }
/>
)
} )
}
</div>
</PanelBody>
</InspectorControls>
)
}

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Button, TextControl } from '@wordpress/components'
import { RichText } from '@wordpress/block-editor'
export default function List( { headings = {}, onHeadingUpdate = {}, edit = {}, toggleEdit = {}, hideHeading = {}, ListStyle = 'ul', isSave = false } ) {
if ( isEmpty( headings ) ) {
return null
}
return (
<>
{ headings.map( ( heading ) => {
if ( isSave && heading.heading.disable ) {
return false
}
const { content, link, disable, key } = heading.heading
const TagName = 'div' === ListStyle ? 'div' : 'li'
return (
<TagName key={ key } className={ disable ? 'disabled' : '' }>
{
isSave &&
<a href={ link }>
{ content }
</a>
}
{
! isSave &&
<RichText
tagName="a"
value={ content }
allowedFormats={ [] }
onChange={ ( newContent ) => onHeadingUpdate( newContent, key, true ) }
placeholder={ __( 'Heading text', 'rank-math' ) }
/>
}
{
heading.children &&
<ListStyle>
<List
headings={ heading.children }
onHeadingUpdate={ onHeadingUpdate }
edit={ edit }
toggleEdit={ toggleEdit }
hideHeading={ hideHeading }
ListStyle={ ListStyle }
isSave={ isSave }
/>
</ListStyle>
}
{
key === edit &&
<TextControl
placeholder={ __( 'Heading Link', 'rank-math' ) }
value={ link }
onChange={ ( newLink ) => onHeadingUpdate( newLink, key ) }
/>
}
{
! isSave &&
<span className="rank-math-block-actions">
<Button
icon={ edit === key ? 'saved' : 'admin-links' }
className="rank-math-item-visbility"
onClick={ () => toggleEdit( edit === key ? false : key ) }
title={ __( 'Edit Link', 'rank-math' ) }
/>
<Button
className="rank-math-item-delete"
icon={ ! disable ? 'visibility' : 'hidden' }
onClick={ () => hideHeading( ! disable, key ) }
title={ __( 'Hide', 'rank-math' ) }
/>
</span>
}
</TagName>
)
} ) }
</>
)
}

View File

@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { isUndefined } from 'lodash'
/**
* WordPress dependencies
*/
import { useBlockProps } from '@wordpress/block-editor'
/**
* Internal dependencies
*/
import { linearToNestedHeadingList } from './utils'
import List from './list'
export default function save( { attributes } ) {
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
return null
}
const TitleWrapper = attributes.titleWrapper
const headings = linearToNestedHeadingList( attributes.headings )
const ListStyle = attributes.listStyle
return (
<div { ...useBlockProps.save() } id="rank-math-toc">
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
<nav>
<ListStyle>
<List
headings={ headings }
ListStyle={ ListStyle }
isSave={ true }
/>
</ListStyle>
</nav>
</div>
)
}

View File

@@ -0,0 +1,34 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { BlockControls } from '@wordpress/block-editor'
import {
Toolbar,
ToolbarButton,
} from '@wordpress/components'
import { formatListBullets, formatListNumbered, alignLeft } from '@wordpress/icons'
export default ( { setAttributes } ) => {
return (
<BlockControls>
<Toolbar label={ __( 'Table of Content Options', 'rank-math' ) }>
<ToolbarButton
icon={ formatListBullets }
label={ __( 'Unordered List', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'ul' } ) }
/>
<ToolbarButton
icon={ formatListNumbered }
label={ __( 'Ordered List', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'ol' } ) }
/>
<ToolbarButton
icon={ alignLeft }
label={ __( 'None', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'div' } ) }
/>
</Toolbar>
</BlockControls>
)
}

View File

@@ -0,0 +1,208 @@
/**
* External dependencies
*/
import { isEmpty, isUndefined, isString, kebabCase, includes, forEach, isEqual, map, isNull } from 'lodash'
/**
* WordPress dependencies
*/
import { store as blockEditorStore } from '@wordpress/block-editor'
import { __unstableStripHTML as stripHTML } from '@wordpress/dom'
import { useSelect, useDispatch } from '@wordpress/data'
// Conditionally generate anchor from heading text.
const generateAnchor = ( anchor, headingText, isGeneratedLink, settings ) => {
if ( ! isEmpty( anchor ) ) {
return anchor
}
if ( ! isUndefined( settings.generateAnchors ) && settings.generateAnchors === true ) {
return anchor
}
return isGeneratedLink ? kebabCase( stripHTML( headingText ) ) : anchor
}
/**
* Get the headings from the content.
*
* @param {Array} headings Array of headings data
* @param {Array} excludeHeadings Heading levels to exclude
*/
export function GetLatestHeadings( headings, excludeHeadings ) {
return useSelect(
( select ) => {
const {
getBlockAttributes,
getBlockName,
getClientIdsWithDescendants,
getSettings,
} = select( blockEditorStore )
const { __experimentalConvertBlockToStatic: convertBlockToStatic } = useDispatch( 'core/reusable-blocks' )
// Get the client ids of all blocks in the editor.
const allBlockClientIds = getClientIdsWithDescendants()
const _latestHeadings = []
let i = 0
const anchors = []
for ( const blockClientId of allBlockClientIds ) {
const blockName = getBlockName( blockClientId )
if ( blockName === 'core/block' ) {
const attrs = getBlockAttributes( blockClientId )
if ( ! isNull( attrs.ref ) ) {
const reusableBlock = wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'wp_block', attrs.ref )
const blocks = map( reusableBlock.blocks, ( block ) => {
return block.name
} )
if ( includes( blocks, 'rank-math/toc-block' ) && ! isNull( getBlockAttributes( blockClientId ) ) ) {
convertBlockToStatic( blockClientId )
}
}
continue
}
if ( ! includes( [ 'rank-math/faq-block', 'rank-math/howto-block', 'core/heading' ], blockName ) ) {
continue
}
const headingAttributes = getBlockAttributes( blockClientId )
if ( blockName === 'rank-math/faq-block' || blockName === 'rank-math/howto-block' ) {
const titleWrapper = headingAttributes.titleWrapper
if (
includes( excludeHeadings, titleWrapper ) ||
includes( [ 'div', 'p' ], titleWrapper )
) {
continue
}
const data = blockName === 'rank-math/howto-block' ? headingAttributes.steps : headingAttributes.questions
if ( isEmpty( data ) ) {
continue
}
forEach( data, ( value ) => {
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
content: '',
level: '',
disable: false,
isUpdated: false,
isGeneratedLink: true,
}
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : value.title
_latestHeadings.push( {
key: value.id,
content: stripHTML( content ),
level: parseInt( headingAttributes.titleWrapper.replace( 'h', '' ) ),
link: ! isGeneratedLink ? currentHeading.link : `#${ value.id }`,
disable: currentHeading.disable ? currentHeading.disable : false,
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
isGeneratedLink,
} )
} )
continue
}
if ( blockName === 'core/heading' ) {
if ( includes( excludeHeadings, 'h' + headingAttributes.level ) ) {
continue
}
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
content: '',
level: '',
disable: false,
isUpdated: false,
isGeneratedLink: true,
}
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
const settings = getSettings()
const headingText = ! isEmpty( headingAttributes.content.text ) ? headingAttributes.content.text : headingAttributes.content
let anchor = generateAnchor( headingAttributes.anchor, headingText, isGeneratedLink, settings )
if ( includes( anchors, anchor ) ) {
i += 1
anchor = anchor + '-' + i
}
anchors.push( anchor )
headingAttributes.anchor = anchor
const headingContent = isString( headingText ) ? stripHTML(
headingText.replace(
/(<br *\/?>)+/g,
' '
)
) : ''
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : headingContent
_latestHeadings.push( {
key: blockClientId,
content: stripHTML( content ),
level: headingAttributes.level,
link: ! isGeneratedLink ? currentHeading.link : `#${ headingAttributes.anchor }`,
disable: currentHeading.disable ? currentHeading.disable : false,
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
isGeneratedLink,
} )
}
}
if ( isEqual( headings, _latestHeadings ) ) {
return null
}
return _latestHeadings
}
)
}
/**
* Nest heading based on the Heading level.
*
* @param {Array} headingList The flat list of headings to nest.
*
* @return {Array} The nested list of headings.
*/
export function linearToNestedHeadingList( headingList = [] ) {
const nestedHeadingList = []
forEach( headingList, ( heading, key ) => {
if ( isEmpty( heading.content ) ) {
return
}
// Make sure we are only working with the same level as the first iteration in our set.
if ( heading.level === headingList[ 0 ].level ) {
if ( headingList[ key + 1 ]?.level > heading.level ) {
let endOfSlice = headingList.length
for ( let i = key + 1; i < headingList.length; i++ ) {
if ( headingList[ i ].level === heading.level ) {
endOfSlice = i
break
}
}
nestedHeadingList.push( {
heading,
children: linearToNestedHeadingList(
headingList.slice( key + 1, endOfSlice )
),
} )
} else {
nestedHeadingList.push( {
heading,
children: null,
} )
}
}
} )
return nestedHeadingList
}

View File

@@ -0,0 +1,54 @@
{
"apiVersion": 3,
"title": "Table of Contents by Rank Math",
"description": "Automatically generate the Table of Contents from the Headings added to this page.",
"name": "rank-math/toc-block",
"category": "rank-math-blocks",
"icon": "rm-icon rm-icon-stories",
"textdomain": "rank-math",
"editorScript": "file:./assets/js/index.js",
"editorStyle": [
"file:./assets/css/toc.css",
"file:./assets/css/toc_list_style.css"
],
"attributes": {
"title": {
"type": "string"
},
"headings": {
"type": "array",
"items": {
"type": "object"
}
},
"listStyle": {
"type": "string"
},
"titleWrapper": {
"type": "string",
"default": "h2"
},
"excludeHeadings": {
"type": "array"
}
},
"supports": {
"color": {
"link": true,
"gradients": true
},
"multiple": false,
"spacing": {
"margin": true,
"padding": true
},
"typography": {
"fontSize": true,
"lineHeight": true,
"__experimentalDefaultControls": {
"fontSize": true
}
},
"align": true
}
}

View File

@@ -0,0 +1,192 @@
<?php
/**
* The TOC Block
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Helpers\Param;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* HowTo Block class.
*/
class Block_TOC extends Block {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/toc-block';
/**
* The single instance of the class.
*
* @var Block_TOC
*/
protected static $instance = null;
/**
* Retrieve main Block_TOC instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_TOC
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_TOC ) ) {
self::$instance = new Block_TOC();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
$this->filter( 'rank_math/schema/block/toc-block', 'add_graph', 10, 2 );
$this->filter( 'render_block_rank-math/toc-block', 'render_toc_block_content', 10, 2 );
$this->filter( 'rank_math/metabox/post/values', 'block_settings_metadata' );
$this->action( 'admin_enqueue_scripts', 'add_json_data' );
register_block_type( RANK_MATH_PATH . 'includes/modules/schema/blocks/toc/block.json' );
$this->action( 'wp_enqueue_scripts', 'register_block_style' );
}
/**
* Register block style.
*/
public function register_block_style() {
wp_register_style( 'rank-math-toc-block', rank_math()->plugin_url() . 'includes/modules/schema/blocks/toc/assets/css/toc_list_style.css', [], rank_math()->version );
}
/**
* Add meta data to use in the TOC block.
*
* @param array $values Aray of tabs.
*
* @return array
*/
public function block_settings_metadata( $values ) {
$values['tocTitle'] = Helper::get_settings( 'general.toc_block_title' );
$values['tocExcludeHeadings'] = Helper::get_settings( 'general.toc_block_exclude_headings', [] );
$values['listStyle'] = Helper::get_settings( 'general.toc_block_list_style', 'ul' );
return $values;
}
/**
* Add default Block values on FSE Template page.
*
* @return void
*/
public function add_json_data() {
if ( Param::get( 'postType' ) !== 'wp_template' ) {
return;
}
Helper::add_json( 'tocTitle', Helper::get_settings( 'general.toc_block_title' ) );
Helper::add_json( 'tocExcludeHeadings', Helper::get_settings( 'general.toc_block_exclude_headings', [] ) );
Helper::add_json( 'listStyle', Helper::get_settings( 'general.toc_block_list_style', 'ul' ) );
}
/**
* Add default TOC title.
*
* @param string $block_content Block content.
* @param array $parsed_block The full block, including name and attributes.
*
* @return string
*/
public function render_toc_block_content( $block_content, $parsed_block ) {
wp_enqueue_style( 'rank-math-toc-block' );
if ( isset( $parsed_block['attrs']['title'] ) ) {
return $block_content;
}
$title = Helper::get_settings( 'general.toc_block_title' );
if ( ! $title ) {
return $block_content;
}
$allowed_tags = [ 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div' ];
$title_wrapper = isset( $parsed_block['attrs']['titleWrapper'] ) && in_array( $parsed_block['attrs']['titleWrapper'], $allowed_tags, true ) ? $parsed_block['attrs']['titleWrapper'] : 'h2';
$block_content = preg_replace_callback(
'/(<div class=".*?wp-block-rank-math-toc-block.*?"\>)/i',
function ( $value ) use ( $title, $block_content, $title_wrapper ) {
if ( ! isset( $value[0] ) ) {
return $block_content;
}
$value[0] = str_replace( '>', ' id="rank-math-toc">', $value[0] );
return $value[0] . '<' . tag_escape( $title_wrapper ) . '>' . esc_html( $title ) . '</' . tag_escape( $title_wrapper ) . '>';
},
$block_content
);
$block_content = str_replace( 'class=""', '', $block_content );
return apply_filters(
'rank_math/schema/block/toc/content',
wp_kses_post( $block_content ),
$block_content,
$parsed_block['attrs'],
);
}
/**
* Add TOC schema data in JSON-LD array.
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
$attributes = $block['attrs'];
// Early bail.
if ( empty( $attributes['headings'] ) ) {
return $data;
}
if ( ! isset( $data['toc'] ) ) {
$data['toc'] = [];
}
foreach ( $attributes['headings'] as $heading ) {
if ( ! empty( $heading['disable'] ) ) {
continue;
}
$data['toc'][] = [
'@context' => 'https://schema.org',
'@type' => 'SiteNavigationElement',
'@id' => '#rank-math-toc',
'name' => esc_html( $heading['content'] ),
'url' => esc_url( get_permalink() . $heading['link'] ),
];
}
if ( empty( $data['toc'] ) ) {
unset( $data['toc'] );
}
return $data;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Blocks general settings.
*
* @package RankMath
* @subpackage RankMath\Schema
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'toc_block_title',
'type' => 'text',
'name' => esc_html__( 'Table of Contents Title', 'rank-math' ),
'desc' => esc_html__( 'Enter the default title to use for the Table of Contents block.', 'rank-math' ),
'classes' => 'rank-math-advanced-option',
'default' => esc_html__( 'Table of Contents', 'rank-math' ),
]
);
$cmb->add_field(
[
'id' => 'toc_block_list_style',
'type' => 'select',
'name' => esc_html__( 'Table of Contents List style', 'rank-math' ),
'desc' => esc_html__( 'Select the default list style for the Table of Contents block.', 'rank-math' ),
'options' => [
'div' => esc_html__( 'None', 'rank-math' ),
'ol' => esc_html__( 'Numbered', 'rank-math' ),
'ul' => esc_html__( 'Unordered', 'rank-math' ),
],
'default' => 'ul',
]
);
$cmb->add_field(
[
'id' => 'toc_block_exclude_headings',
'name' => esc_html__( 'Table of Contents Exclude Headings', 'rank-math' ),
'desc' => esc_html__( 'Choose the headings to exclude from the Table of Contents block.', 'rank-math' ),
'type' => 'multicheck',
'options' => [
'h1' => esc_html__( 'Heading H1', 'rank-math' ),
'h2' => esc_html__( 'Heading H2', 'rank-math' ),
'h3' => esc_html__( 'Heading H3', 'rank-math' ),
'h4' => esc_html__( 'Heading H4', 'rank-math' ),
'h5' => esc_html__( 'Heading H5', 'rank-math' ),
'h6' => esc_html__( 'Heading H6', 'rank-math' ),
],
]
);