DigiPress

Highly Flexible WordPress Theme

[WP]コアブロックに任意の class と style を追加する方法

カスタマイズ・技術情報

WordPress(Gutenberg) の標準ブロックの親要素に任意の class を挿入するために、以前、元のブロックを class を引き継いで div でラップする少々トリッキーな方法をご紹介しました。

しかし、WordPress 5.9 以降では、この方法でも「Block validation: Block validation failed for…」のエラーが返されるようになりました。

そこで、今回は editor.BlockListBlock フィルター(エディター側)と blocks.getSaveContent.extraProps フィルター(保存時)を利用した正攻法で実装してみます。


カスタム属性を追加する

前段階として、WordPress の任意のブロックをカスタマイズするための独自のカスタム属性を blocks.registerBlockType フィルターを利用して追加します。

ここでは、標準ブロックである「テーブル」に全セルのテキストを一括で水平、垂直別に中央寄せするためのオプション(追加 class)と、テーブルの下に指定したサイズのスペース(インライン style)を空けるためのオプションを例として追加してみます。

import { addFilter } from '@wordpress/hooks';

// 対象とするブロック
const allowedBlocks = [ 'core/table' ];

const addExtraAttributes = ( settings ) => {
	// テーブルブロックだったら
	if ( allowedBlocks.includes( settings.name ) ) {
		// horizontalAlignCenter, verticalAlignCenter, bottomSpace という属性を追加
		settings.attributes = Object.assign( settings.attributes, {
			horizontalAlignCenter: {
				type: 'boolean',
				default: false,
			},
			verticalAlignCenter: {
				type: 'boolean',
				default: false,
			},
			bottomSpace: {
				type: 'string',
				default:'',
			}
		} );
	}
	return settings;
}

addFilter(
	'blocks.registerBlockType',
	'my-name-space/add-extra-attributes',
	addExtraAttributes,
);

エディターでカスタム属性を操作するためのコントロールを追加

追加したカスタム属性の値を、ユーザーがエディターから操作するためのコントロール(InspectorControls)を editor.BlockEdit フィルターを利用してテーブルブロックに組み込みます。

import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { Fragment } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, PanelRow } from '@wordpress/components'; 
import { __experimentalUnitControl as UnitControl } from '@wordpress/components';

const allowedBlocks = [ 'core/table' ];

const addCustomControls = createHigherOrderComponent( ( BlockEdit ) => {
	return ( props ) => {
		if ( allowedBlocks.includes( props.name ) ) {
			const {
				setAttributes,
				isSelected,
				attributes
			} = props;

			const {
				className,
				horizontalAlignCenter,
				verticalAlignCenter,
				bottomSpace
			} = attributes;

			const unitControlProps = ( label, labelPosition, value ) => {
				return {
					label: label,
					labelPosition: labelPosition,
					value: value,
					units: [
						{ value: 'px', label: 'px', default: 0 },
						{ value: '%', label: '%', default: 0 },
						{ value: 'vw', label: 'vw', default: 0 },
						{ value: 'vh', label: 'vh', default: 0 },
					],
					__unstableInputWidth: '80px',
				}
			}

			return (
				<Fragment>
					<BlockEdit { ...props } />

					{ isSelected &&
						<InspectorControls>
							<PanelBody
								title= { __( 'Advanced Settings' ) }
								initialOpen={ false }>
								<ToggleControl
									label={ __( 'Center all cells horizontally' ) }
									checked={ horizontalAlignCenter }
									onChange={ ( horizontalAlignCenter ) => setAttributes( { horizontalAlignCenter } ) }
								/>
								<ToggleControl
									label={ __( 'Center all cells vertically' ) }
									checked={ verticalAlignCenter }
									onChange={ ( verticalAlignCenter ) => setAttributes( { verticalAlignCenter } ) }
								/>

								<PanelRow>
									<UnitControl
										{ ...unitControlProps( __( 'Block bottom space' ), 'top', bottomSpace ) }
										onChange={ ( bottomSpace ) => setAttributes( { bottomSpace } ) }
									/>
								</PanelRow>
							</PanelBody>
						</InspectorControls>
					}
				</Fragment>
			);
		}

		return <BlockEdit { ...props } />;
	}
}, 'addCustomControls' );

addFilter(
	'editor.BlockEdit',
	'my-name-space/add-custom-contorols',
	addCustomControls,
);

これでカスタム属性の追加とそれをエディター側で操作するための準備が整いました。

対象とするブロックは、allowedBlocks 定数にまとめておきます。
ここでは 'core/table' のみがありますが、複数ブロックで共通のカスタム属性としたい場合は、この allowedBlocks の配列に対象ブロック名を追加します。

ブロック下部のスペース(margin-bottom)は、UnitControl コンポーネントを利用して単位を px, %, vw, vh から選べるようにしています。

続いて、これらのカスタム属性の値を元にして、テーブルブロックのラッパー要素の class と style に独自の定義を追加する処理を用意します。

[エディター]ブロックの親要素に class と style を追加

editor.BlockListBlock フィルターを利用して、カスタム属性が変更された際にエディターの対象ブロックにそれを反映させるために、親要素の class と style 属性に追加します。

import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import classnames from 'classnames';

const applyExtraAttributesInEditor = createHigherOrderComponent( ( BlockListBlock ) => {
	return ( props ) => {
		const {
			attributes,
			className,
			name,
			isValid,
			wrapperProps
		} = props;

		if ( isValid && allowedBlocks.includes( name ) ){
			const {
				horizontalAlignCenter,
				verticalAlignCenter,
				bottomSpace
			} = attributes;

			// 値があれば class を追加
			const extraClass = [ {
				'center-horizontally': horizontalAlignCenter,
				'center-vertically': verticalAlignCenter,
			} ]

			// 値があれば margin-bottom の値を追加
			const extraStyle = {
				marginBottom: bottomSpace ? bottomSpace : undefined
			}

			let blockWrapperProps = wrapperProps;
			blockWrapperProps = {
				...blockWrapperProps,
				style: {
					...( blockWrapperProps && { ...blockWrapperProps.style } ),
					...extraStyle
				},
			};

			return (
				<BlockListBlock { ...props }
					className={ classnames( extraClass, className ) }
					wrapperProps={ blockWrapperProps } />
			);
		}

		return (
			<BlockListBlock { ...props } />
		);
	};
}, 'applyExtraAttributesInEditor' );

addFilter(
	'editor.BlockListBlock',
	'my-name-space/apply-extra-attributes-in-editor',
	applyExtraAttributesInEditor,
);

horizontalAlignCenter 属性が有効の場合は .center-horizontally という class が追加され、verticalAlignCenter 属性が有効の場合は .center-vertically という class が追加されます。

ブロック下部のスペースは、bottomSpace 属性の値を margin-bottom に渡しています。

中央寄せの CSS

このデモでは、テーブルブロックの全てのセル内のテキストの配置を一括で水平または垂直方向にセンタリングするカスタム属性を追加するため、水平方向の中央寄せのトリガーとなる.center-horizontally というセレクタと、垂直方向の中央寄せのトリガーとなる .center-vertically というセレクタに必要な CSS を定義しておきます。

SCSS
.wp-block-table{
	&.center-horizontally{
		th, td{
			text-align: center;
		}
	}
	&.center-vertically{
		th, td{
			vertical-align: middle;
		}
	}
}

[フロントエンド]ブロックの親要素に class と style を追加

同様に、フロントエンド側(保存時)にも反映させるため、blocks.getSaveContent.extraProps フィルターを利用して対象ブロックの親要素に class と style 属性を追加します。

import { addFilter } from '@wordpress/hooks';
import classnames from 'classnames';

const applyExtraAttributesInFrontEnd = ( props, blockType, attributes  ) => {

	if ( allowedBlocks.includes( blockType.name ) ) {
		const {
			setAttributes,
			isSelected,
			attributes
		} = props;

		const {
			className,
			horizontalAlignCenter,
			verticalAlignCenter,
			bottomSpace
		} = attributes;

		const extraClass = [ {
			'center-horizontally': horizontalAlignCenter,
			'center-vertically': verticalAlignCenter,
		} ]

		const extraStyle = {
			marginBottom: bottomSpace ? bottomSpace : undefined
		}

		props.className = classnames( props.className, extraClass );

		return Object.assign( props, { style: { ...props.style, ...extraStyle } } );
	}

	return props;
}

addFilter(
	'blocks.getSaveContent.extraProps',
	'my-name-space/apply-extra-attributes-in-front-end',
	applyExtraAttributesInFrontEnd,
);

以上で、対象のブロックへエディターとフロントエンドの両方にカスタム属性を元にした任意の class と style が追加されるようになります。

まとめる

ここまでの処理をまとめると、以下のようになります。

import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { Fragment } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, PanelRow } from '@wordpress/components'; 
import { __experimentalUnitControl as UnitControl } from '@wordpress/components';
import classnames from 'classnames';

const allowedBlocks = [ 'core/table' ];

const addExtraAttributes = ( settings ) => {

	if ( allowedBlocks.includes( settings.name ) ) {
		settings.attributes = Object.assign( settings.attributes, {
			horizontalAlignCenter: {
				type: 'boolean',
				default: false,
			},
			verticalAlignCenter: {
				type: 'boolean',
				default: false,
			},
			bottomSpace: {
				type: 'string',
				default:'',
			}
		} );
	}

	return settings;
}

const addCustomControls = createHigherOrderComponent( ( BlockEdit ) => {

	return ( props ) => {
		if ( allowedBlocks.includes( props.name ) ) {
			const {
				setAttributes,
				isSelected,
				attributes
			} = props;

			const {
				className,
				horizontalAlignCenter,
				verticalAlignCenter,
				bottomSpace
			} = attributes;

			const unitControlProps = ( label, labelPosition, value ) => {
				return {
					label: label,
					labelPosition: labelPosition,
					value: value,
					units: [
						{ value: 'px', label: 'px', default: 0 },
						{ value: '%', label: '%', default: 0 },
						{ value: 'vw', label: 'vw', default: 0 },
						{ value: 'vh', label: 'vh', default: 0 },
					],
					__unstableInputWidth: '80px',
				}
			}

			return (
				<Fragment>
					<BlockEdit { ...props } />

					{ isSelected &&
						<InspectorControls>
							<PanelBody
								title= { __( 'Advanced Settings' ) }
								initialOpen={ false }>
								<ToggleControl
									label={ __( 'Center all cells horizontally' ) }
									checked={ horizontalAlignCenter }
									onChange={ ( horizontalAlignCenter ) => setAttributes( { horizontalAlignCenter } ) }
								/>
								<ToggleControl
									label={ __( 'Center all cells vertically' ) }
									checked={ verticalAlignCenter }
									onChange={ ( verticalAlignCenter ) => setAttributes( { verticalAlignCenter } ) }
								/>

								<PanelRow>
									<UnitControl
										{ ...unitControlProps( __( 'Block bottom space' ), 'top', bottomSpace ) }
										onChange={ ( bottomSpace ) => setAttributes( { bottomSpace } ) }
									/>
								</PanelRow>
							</PanelBody>
						</InspectorControls>
					}
				</Fragment>
			);
		}

		return <BlockEdit { ...props } />;
	}
}, 'addCustomControls' );

const applyExtraAttributesInEditor = createHigherOrderComponent( ( BlockListBlock ) => {

	return ( props ) => {

		const {
			attributes,
			className,
			name,
			isValid,
			wrapperProps
		} = props;

		if ( isValid && allowedBlocks.includes( name ) ){

			const {
				horizontalAlignCenter,
				verticalAlignCenter,
				bottomSpace
			} = attributes;

			const extraClass = [ {
				'center-horizontally': horizontalAlignCenter,
				'center-vertically': verticalAlignCenter,
			} ]

			const extraStyle = {
				marginBottom: bottomSpace ? bottomSpace : undefined
			}

			let blockWrapperProps = wrapperProps;
			blockWrapperProps = {
				...blockWrapperProps,
				style: {
					...( blockWrapperProps && { ...blockWrapperProps.style } ),
					...extraStyle
				},
			};

			return (
				<BlockListBlock { ...props }
					className={ classnames( className, extraClass ) }
					wrapperProps={ blockWrapperProps } />
			);
		}

		return (
			<BlockListBlock { ...props } />
		);
	};
}, 'applyExtraAttributesInEditor' );

const applyExtraAttributesInFrontEnd = ( props, blockType, attributes  ) => {

	if ( allowedBlocks.includes( blockType.name ) ) {
		const {
			setAttributes,
			isSelected,
			attributes
		} = props;

		const {
			className,
			horizontalAlignCenter,
			verticalAlignCenter,
			bottomSpace
		} = attributes;

		const extraClass = [ {
			'center-horizontally': horizontalAlignCenter,
			'center-vertically': verticalAlignCenter,
		} ]

		const extraStyle = {
			marginBottom: bottomSpace ? bottomSpace : undefined
		}

		props.className = classnames( props.className, extraClass );

		return Object.assign( props, { style: { ...props.style, ...extraStyle } } );
	}

	return props;
}

addFilter(
	'blocks.registerBlockType',
	'my-name-space/add-extra-attributes',
	addExtraAttributes,
);

addFilter(
	'editor.BlockEdit',
	'my-name-space/add-custom-contorols',
	addCustomControls,
);

addFilter(
	'editor.BlockListBlock',
	'my-name-space/apply-extra-attributes-in-editor',
	applyExtraAttributesInEditor,
);

addFilter(
	'blocks.getSaveContent.extraProps',
	'my-name-space/apply-extra-attributes-in-front-end',
	applyExtraAttributesInFrontEnd,
);

各種ブロックフィルターの詳細については公式ドキュメントを参考にしてください。

Share / Subscribe
Facebook
Posts
Hatena Bookmarks
Pinterest
Pocket
Feedly
Send to LINE
AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo3MmI1NmY0Mi1mOTkxLTRhZWMtYjI3Zi05NmQ2Y2Q2ZGJmZWIAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaJpFkg1UlHpZNlVv23V+twUAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpiYjI4ZjNiNS02YWYyLTQzZDAtOTdhYi05Y2U4N2E3MzNlMGFscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo5gNfqq3PzirY5Ex0DoWaEwAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggRmYWoSvnUaiaZo0ZsQM96LtvWKoYvEtwD1ZLsm7yrrekZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaKpVotd9NvpSskj5KUcCIi0AAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCAosBvMqF1+lZe9gmOcHC/J3Y6+rBHLvQJxpFh59mfTKGRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjaZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjcyYjU2ZjQyLWY5OTEtNGFlYy1iMjdmLTk2ZDZjZDZkYmZlYi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjM2MjQxODYwLTVjZjQtNGFiNy1iMTdkLTgzY2I1NGQ4Mjk1Y3JjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBGZhahK+dRqJpmjRmxAz3ou29Yqhi8S3APVkuybvKut6JjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggGA0lysfzsAP33kpMa/AM72oz/8ZsEHu9XyDWwTO5v22iY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggLW0SxOkC51fl8mcYUAJVo/Rm2FBikfsY/PunAD9daDd0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQOX0HxJBDEy+kPFgsqx5au8boT29bL+Ruh52/5DgQoYrU+ZyLgBowbwVTBBjsXdI/XKzdHkn4W7kxyR1u/Wk548=