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 @@
@-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;
}
}