diff --git a/src/css/edit.scss b/src/css/edit.scss index 546c2660..036e4dec 100644 --- a/src/css/edit.scss +++ b/src/css/edit.scss @@ -152,3 +152,9 @@ p.snippet-scope, .snippet-scope p { .wrap .notice { scroll-margin: 0.75em; } + +#edit-snippet-form-container .cs-sticky-notice { + position: sticky; + top: 40px; + z-index: 100; +} diff --git a/src/css/manage.scss b/src/css/manage.scss index 74abbaad..cfd55920 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -72,20 +72,12 @@ $inactive-color: #ccc; border-radius: 50%; } - &:hover::before { - transform: translateX(40%); - } - .snippets .active-snippet & { background-color: $active-color; &::before { transform: translateX(100%); } - - &:hover::before { - transform: translateX(60%); - } } .snippets .erroneous-snippet &::before { diff --git a/src/js/components/SnippetForm/fields/ScopeInput.tsx b/src/js/components/SnippetForm/fields/ScopeInput.tsx index bc1c5659..7ef9ae4f 100644 --- a/src/js/components/SnippetForm/fields/ScopeInput.tsx +++ b/src/js/components/SnippetForm/fields/ScopeInput.tsx @@ -7,6 +7,7 @@ import { buildShortcodeTag } from '../../../utils/shortcodes' import { getSnippetType } from '../../../utils/snippets' import { CopyToClipboardButton } from '../../common/CopyToClipboardButton' import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { truncateWords } from '../../../utils/text' import type { ShortcodeAtts } from '../../../utils/shortcodes' import type { SnippetScope } from '../../../types/Snippet' import type { Dispatch, SetStateAction} from 'react' @@ -117,6 +118,7 @@ const ShortcodeInfo: React.FC = () => { <> diff --git a/src/js/components/SnippetForm/page/Notices.tsx b/src/js/components/SnippetForm/page/Notices.tsx index 43d5eaae..83c9d699 100644 --- a/src/js/components/SnippetForm/page/Notices.tsx +++ b/src/js/components/SnippetForm/page/Notices.tsx @@ -2,28 +2,31 @@ import classnames from 'classnames' import React, { useEffect } from 'react' import { __, sprintf } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' -import type { MouseEventHandler, ReactNode} from 'react' +import type { ReactNode } from 'react' interface DismissibleNoticeProps { classNames?: classnames.Argument - onRemove: MouseEventHandler + onRemove: VoidFunction children?: ReactNode + autoHide?: boolean } -const DismissibleNotice: React.FC = ({ classNames, onRemove, children }) => { +const DismissibleNotice: React.FC = ({ classNames, onRemove, children, autoHide = true }) => { useEffect(() => { - if (window.CODE_SNIPPETS_EDIT?.scrollToNotices) { - window.scrollTo({ top: 0, behavior: 'smooth' }) + if (autoHide) { + const timer = setTimeout(onRemove, 5000) + return () => clearTimeout(timer) } - }, []) + return undefined + }, [autoHide, onRemove]) return ( -
+
<>{children} @@ -45,6 +48,7 @@ export const Notices: React.FC = () => { setSnippet(previous => ({ ...previous, code_error: null }))} + autoHide={false} >

{sprintf( diff --git a/src/js/types/Shortcodes.ts b/src/js/types/Shortcodes.ts index 013ea7de..edb63d99 100644 --- a/src/js/types/Shortcodes.ts +++ b/src/js/types/Shortcodes.ts @@ -5,6 +5,7 @@ export interface SourceShortcodeAtts { export interface ContentShortcodeAtts { id: string + name: string php: boolean format: boolean shortcodes: boolean diff --git a/src/js/utils/text.ts b/src/js/utils/text.ts index 590dac66..98376115 100644 --- a/src/js/utils/text.ts +++ b/src/js/utils/text.ts @@ -6,3 +6,10 @@ export const trimLeadingChar = (text: string, character: string): string => export const trimTrailingChar = (text: string, character: string): string => character === text.charAt(text.length - 1) ? text.slice(0, -1) : text + +export const truncateWords = (text: string, wordCount: number = 3): string => { + const words = text.trim().split(/\s+/); + return words.length > wordCount + ? `${words.slice(0, wordCount).join(' ')}...` + : text; +}; \ No newline at end of file diff --git a/src/php/front-end/class-front-end.php b/src/php/front-end/class-front-end.php index 4a3407d4..91aae904 100644 --- a/src/php/front-end/class-front-end.php +++ b/src/php/front-end/class-front-end.php @@ -28,6 +28,11 @@ class Front_End { */ const PRISM_HANDLE = 'code-snippets-prism'; + /** + * Maximum depth for shortcode recursion. + */ + const MAX_SHORTCODE_DEPTH = 5; + /** * Class constructor */ @@ -235,7 +240,7 @@ protected function convert_boolean_attribute_flags( array $atts, array $boolean_ * * @return string Evaluated shortcode content. */ - protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): string { + protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): string { if ( empty( $atts['php'] ) ) { return $snippet->code; } @@ -325,8 +330,25 @@ public function render_content_shortcode( array $atts ): string { // Remove this shortcode from the list to prevent recursion. remove_shortcode( self::CONTENT_SHORTCODE ); - // Evaluate shortcodes. - $content = do_shortcode( $atts['format'] ? shortcode_unautop( $content ) : $content ); + // Recursion depth is limited to prevent infinite loops. + static $depth = 0; + $max_depth = self::MAX_SHORTCODE_DEPTH; + + // Find the shortcode in the content and replace it with the evaluated content. + $content = preg_replace_callback( + '/\[' . self::CONTENT_SHORTCODE . '([^\]]*)\]/', + function ($matches) use (&$depth, $max_depth) { + if ($depth >= $max_depth) { + return ''; + } + $depth++; + $atts = shortcode_parse_atts($matches[1]); + $result = $this->render_content_shortcode($atts); + $depth--; + return $result; + }, + $content + ); // Add this shortcode back to the list. add_shortcode( self::CONTENT_SHORTCODE, [ $this, 'render_content_shortcode' ] );