Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions classes/controllers/FrmStylesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -844,14 +844,25 @@ private static function manage_styles() {
* Echo content for the Custom CSS page.
*
* @param string $message
* @param array $extra_args An array of extra arguments.
*
* @return void
*/
public static function custom_css( $message = '' ) {
$settings = self::enqueue_codemirror();
$id = $settings ? 'frm_codemirror_box' : 'frm_custom_css_box';
$custom_css = self::get_custom_css();
public static function custom_css( $message = '', $extra_args = array() ) {
$id = $extra_args['id'] ?? 'frm_codemirror_box';
$settings = self::enqueue_codemirror( $id, $extra_args['placeholder'] ?? '' );
$id = $settings ? $id : 'frm_custom_css_box';
$show_errors = $extra_args['show_errors'] ?? true;
$custom_css = $extra_args['custom_css'] ?? self::get_custom_css();
$heading = $extra_args['heading'] ?? __( 'You can add custom css here or in your theme style.css. Any CSS added here will be used anywhere the Formidable CSS is loaded.', 'formidable' );
$textarea_params = ! empty( $extra_args['textarea_params'] ) ? $extra_args['textarea_params'] : array(
'name' => 'frm_custom_css',
'id' => $id,
);

if ( ! empty( $settings ) ) {
$textarea_params['class'] = 'hide-if-js';
}
include FrmAppHelper::plugin_path() . '/classes/views/styles/custom_css.php';
}

Expand All @@ -860,9 +871,16 @@ public static function custom_css( $message = '' ) {
*
* @since 6.0
*
* @param array|null $single_style_settings The single style settings.
*
* @return string
*/
public static function get_custom_css() {
public static function get_custom_css( $single_style_settings = null ) {
// If the single style settings are passed, return the custom CSS from the single style settings.
if ( ! empty( $single_style_settings['single_style_custom_css'] ) && ! empty( $single_style_settings['enable_style_custom_css'] ) ) {
return $single_style_settings['single_style_custom_css'];
}
Comment thread
Liviu-p marked this conversation as resolved.

$settings = FrmAppHelper::get_settings();

if ( is_string( $settings->custom_css ) ) {
Expand All @@ -883,9 +901,12 @@ public static function get_custom_css() {
*
* @since 6.0 Previously this code was embedded in self::custom_css.
*
* @param string $id The ID of the codemirror box.
* @param string $placeholder The placeholder text for the codemirror box.
*
* @return array|false
*/
private static function enqueue_codemirror() {
private static function enqueue_codemirror( $id = 'frm_codemirror_box', $placeholder = '' ) {
Comment thread
Crabcyborg marked this conversation as resolved.
if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
// The WordPress version is likely older than 4.9.
return false;
Expand All @@ -900,6 +921,7 @@ private static function enqueue_codemirror() {
// As the codemirror box only appears once you click into the Custom CSS tab, we need to auto-refresh.
// Otherwise the line numbers all end up with a 1px width causing overlap issues with the text in the content.
'autoRefresh' => true,
'placeholder' => $placeholder,
),
)
);
Expand All @@ -908,7 +930,9 @@ private static function enqueue_codemirror() {
wp_add_inline_script(
'code-editor',
sprintf(
'jQuery( function() { wp.codeEditor.initialize( \'frm_codemirror_box\', %s ); } );',
'jQuery( function() { window.%s_wp_editor = wp.codeEditor.initialize( \'%s\', %s ); } );',
$id,
$id,
wp_json_encode( $settings )
)
);
Expand Down
262 changes: 262 additions & 0 deletions classes/helpers/FrmCssScopeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
die( 'You are not allowed to call this page directly.' );
}

class FrmCssScopeHelper {

/**
* Nest the CSS.
* This function nests the CSS by adding the class name prefix to the selectors.
*
* @param string $css
* @param string $class_name
*
* @return string
Comment thread
Liviu-p marked this conversation as resolved.
*/
public function nest( $css, $class_name ) {
Comment thread
Crabcyborg marked this conversation as resolved.
// Remove CSS comments but preserve newlines
$css = preg_replace( '/\/\*.*?\*\//s', '', $css );

$output = array();
$css = trim( $css );
$length = strlen( $css );
$i = 0;
$buffer = '';

while ( $i < $length ) {
$char = $css[ $i ];

if ( '@' === $char ) {
$brace_pos = strpos( $css, '{', $i );

if ( false === $brace_pos ) {
$buffer .= $char;
++$i;
continue;
}

$rule = substr( $css, $i, $brace_pos - $i );
$closing_brace = $this->find_matching_brace( $css, $brace_pos );
$inner_content = substr( $css, $brace_pos + 1, $closing_brace - $brace_pos - 1 );

// Don't nest keyframes content
if ( strpos( $rule, '@keyframes' ) !== false ) {
$output[] = "\n" . $rule . ' {' . $inner_content . '}' . "\n";
} else {
$output[] = "\n" . $rule . ' {';
$output[] = $this->nest( $inner_content, $class_name );
$output[] = '}' . "\n";
}
Comment thread
Liviu-p marked this conversation as resolved.

$i = $closing_brace + 1;
$buffer = '';
continue;
}//end if
Comment thread
Crabcyborg marked this conversation as resolved.
Comment thread
Liviu-p marked this conversation as resolved.

if ( '{' === $char ) {
$selector = trim( $buffer );
$closing_brace = $this->find_matching_brace( $css, $i );
$declarations = substr( $css, $i + 1, $closing_brace - $i - 1 );

// Preserve indentation and formatting of declarations
$declarations = $this->preserve_declaration_formatting( $declarations );

if ( '' !== $selector && '' !== trim( $declarations ) ) {
// Handle multiple selectors
$selectors = array_map( 'trim', explode( ',', $selector ) );
$prefixed_selectors = array();

foreach ( $selectors as $single_selector ) {
if ( '' !== $single_selector ) {
$prefixed_selectors[] = '.' . $class_name . ' ' . $single_selector;
}
}

if ( ! empty( $prefixed_selectors ) ) {
$output[] = "\n" . implode( ',' . "\n", $prefixed_selectors ) . ' {' . $declarations . '}' . "\n";
}
}

$i = $closing_brace + 1;
$buffer = '';
continue;
}//end if

$buffer .= $char;
++$i;
}//end while

return implode( '', $output );
}

/**
* Unnest the CSS.
* This function unnests the CSS by removing the class name prefix from the selectors.
*
* @param string $css
* @param string $class_name
*
* @return string
*/
public function unnest( $css, $class_name ) {
// Remove CSS comments but preserve newlines
$css = preg_replace( '/\/\*.*?\*\//s', '', $css );

$output = array();
$css = trim( $css );
$length = strlen( $css );
$i = 0;
$buffer = '';
$prefix = '.' . $class_name . ' ';
$prefix_length = strlen( $prefix );

while ( $i < $length ) {
$char = $css[ $i ];

if ( '@' === $char ) {
$brace_pos = strpos( $css, '{', $i );

if ( false === $brace_pos ) {
$buffer .= $char;
++$i;

continue;
}

$rule = substr( $css, $i, $brace_pos - $i );
$closing_brace = $this->find_matching_brace( $css, $brace_pos );
$inner_content = substr( $css, $brace_pos + 1, $closing_brace - $brace_pos - 1 );

$output[] = "\n" . $rule . ' {';
$output[] = $this->unnest( $inner_content, $class_name );
$output[] = '}' . "\n";

$i = $closing_brace + 1;
$buffer = '';
continue;
}//end if

if ( '{' === $char ) {
$selector = trim( $buffer );
$closing_brace = $this->find_matching_brace( $css, $i );
$declarations = substr( $css, $i + 1, $closing_brace - $i - 1 );

// Preserve indentation and formatting of declarations
$declarations = $this->preserve_declaration_formatting( $declarations );

if ( '' !== $selector && '' !== trim( $declarations ) ) {
// Handle multiple selectors
$selectors = array_filter(
array_map( 'trim', explode( ',', $selector ) ),
function ( $s ) {
return '' !== $s;
}
);
$unprefixed_selectors = array();

foreach ( $selectors as $single_selector ) {
$unprefixed_selectors[] = 0 === strpos( $single_selector, $prefix )
? trim( substr( $single_selector, $prefix_length ) )
: $single_selector;
}

if ( ! empty( $unprefixed_selectors ) ) {
$output[] = "\n" . implode( ',' . "\n", $unprefixed_selectors ) . ' {' . $declarations . '}' . "\n";
}
}

$i = $closing_brace + 1;
$buffer = '';
continue;
}//end if

$buffer .= $char;
++$i;
}//end while
return implode( '', $output );
}

/**
* Preserve declaration formatting with proper indentation.
*
* @param string $declarations
*
* @return string
*/
private function preserve_declaration_formatting( $declarations ) {
// Trim the entire block but keep internal structure
$declarations = trim( $declarations );

if ( '' === $declarations ) {
return '';
}

// Check if declarations are already on multiple lines
if ( strpos( $declarations, "\n" ) !== false ) {
// Already formatted - preserve it
$lines = explode( "\n", $declarations );
$formatted_lines = array();

foreach ( $lines as $line ) {
$trimmed = trim( $line );

if ( '' !== $trimmed ) {
$formatted_lines[] = "\n\t" . $trimmed;
}
}

return implode( '', $formatted_lines ) . "\n";
}

// Single line - add minimal formatting
return ' ' . $declarations . ' ';
}

/**
* Find the matching brace in the CSS.
*
* @param string $css
* @param int $open_pos
*
* @return int
*/
private function find_matching_brace( $css, $open_pos ) {
$level = 1;
$length = strlen( $css );
$in_string = false;
$string_char = '';

for ( $i = $open_pos + 1; $i < $length; $i++ ) {
$char = $css[ $i ];

// Handle string literals to avoid matching braces inside strings
if ( ( '"' === $char || "'" === $char ) && ( 0 === $i || '\\' !== $css[ $i - 1 ] ) ) {
if ( ! $in_string ) {
$in_string = true;
$string_char = $char;
} elseif ( $char === $string_char ) {
$in_string = false;
}
continue;
}

// Skip braces inside strings
if ( $in_string ) {
continue;
}

if ( '{' === $char ) {
++$level;
} elseif ( '}' === $char ) {
--$level;

if ( 0 === $level ) {
return $i;
}
}
}//end for

return $length - 1;
}
}//end class
13 changes: 11 additions & 2 deletions classes/models/FrmStyle.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ public function duplicate( $id ) {
* @return array<int|WP_Error>
*/
public function update( $id = 'default' ) {
$all_instances = $this->get_all();
$all_instances = $this->get_all();
$css_scope_helper = new FrmCssScopeHelper();

if ( ! $id ) {
$new_style = (array) $this->get_new();
Expand Down Expand Up @@ -118,6 +119,11 @@ public function update( $id = 'default' ) {
$new_instance['post_content']['custom_css'] = $custom_css;
unset( $custom_css );

if ( ! empty( $new_instance['post_content']['single_style_custom_css'] ) ) {
$css_scope = 'frm_style_' . $new_instance['post_name'];
$new_instance['post_content']['single_style_custom_css'] = $css_scope_helper->nest( $new_instance['post_content']['single_style_custom_css'], $css_scope );
}
Comment thread
Liviu-p marked this conversation as resolved.

$new_instance['post_type'] = FrmStylesController::$post_type;
$new_instance['post_status'] = 'publish';

Expand Down Expand Up @@ -265,7 +271,7 @@ public function sanitize_post_content( $settings ) {
$sanitized_settings[ $key ] = $defaults[ $key ];
}

if ( 'custom_css' !== $key ) {
if ( 'custom_css' !== $key && 'single_style_custom_css' !== $key ) {
$sanitized_settings[ $key ] = $this->strip_invalid_characters( $sanitized_settings[ $key ] );
}
}
Expand Down Expand Up @@ -778,6 +784,9 @@ public function get_defaults() {
'use_base_font_size' => false,
'base_font_size' => '15px',
'field_shape_type' => 'rounded-corner',

'enable_style_custom_css' => false,
'single_style_custom_css' => '',
);

return apply_filters( 'frm_default_style_settings', $defaults );
Expand Down
Loading