\n\n \n \n";
}
/**
* Extract widget snippet.
*
* @param SimpleXMLElement $xml
* @param string $name
* @return string|null
*/
public function extract_widget_snippet( SimpleXMLElement $xml, string $name ) {
if ( isset( $xml->{'elementor-widgets'}->widget ) ) {
foreach ( $xml->{'elementor-widgets'}->widget as $wdg ) {
if ( (string) $wdg['name'] === $name ) {
return self::wrap_node_in_config( $wdg, $this->section_name );
}
}
}
return null;
}
/**
* Convert flat path list into nested tree.
*
* @param array $field_details
* @return array
*/
public function build_fields_tree( array $field_details ): array {
$tree = array();
foreach ( $field_details as $path => $info ) {
$delimiter = false !== strpos( (string) $path, '>' ) ? '>' : ( false !== strpos( (string) $path, '.' ) ? '.' : '' );
$segments = empty( $delimiter ) ? array( (string) $path ) : explode( $delimiter, (string) $path );
$cursor = &$tree;
$len = count( $segments );
foreach ( $segments as $i => $segment ) {
$segment = trim( $segment );
if ( '' === $segment ) {
continue;
}
if ( $i === $len - 1 ) {
if ( ! isset( $cursor[ $segment ] ) || ! is_array( $cursor[ $segment ] ) ) {
$cursor[ $segment ] = array();
}
$cursor[ $segment ]['__leaf__'] = $info;
} else {
if ( ! isset( $cursor[ $segment ] ) || ! is_array( $cursor[ $segment ] ) ) {
$cursor[ $segment ] = array();
}
$cursor = &$cursor[ $segment ];
}
}
}
return $tree;
}
/**
* Recursively append tree nodes into DOMElement.
*
* @param DOMDocument $dom
* @param DOMElement $parent_element
* @param array $tree
*/
private function append_tree_to_dom( DOMDocument $dom, DOMElement $parent_element, array $tree ): void {
foreach ( $tree as $key => $sub_tree ) {
$f_node = $dom->createElement( 'field' );
$f_node->setAttribute( 'key', (string) $key );
if ( isset( $sub_tree['__leaf__'] ) ) {
$info = $sub_tree['__leaf__'];
$f_node->setAttribute( 'type', $info['type'] ?? 'title' );
$f_node->setAttribute( 'editor_type', $info['editor_type'] ?? 'LINE' );
if ( ! empty( $info['noTranslate'] ) ) {
$f_node->setAttribute( 'noTranslate', 'true' );
}
}
$has_children = false;
foreach ( $sub_tree as $child_key => $child_val ) {
if ( '__leaf__' !== $child_key ) {
$has_children = true;
break;
}
}
if ( $has_children ) {
$child_tree = array_filter(
$sub_tree,
function ( $k ) {
return '__leaf__' !== $k;
},
ARRAY_FILTER_USE_KEY
);
$this->append_tree_to_dom( $dom, $f_node, $child_tree );
}
$parent_element->appendChild( $f_node );
}
}
/**
* Recursively append tree nodes into SimpleXMLElement.
*
* @param SimpleXMLElement $parent_node
* @param array $tree
*/
private function append_tree_to_simplexml( SimpleXMLElement $parent_node, array $tree ): void {
foreach ( $tree as $key => $sub_tree ) {
$target_node = null;
if ( isset( $parent_node->field ) ) {
foreach ( $parent_node->field as $fld ) {
$fld_key = isset( $fld['key'] ) ? (string) $fld['key'] : trim( (string) $fld );
if ( $fld_key === (string) $key ) {
$target_node = $fld;
break;
}
}
}
if ( ! $target_node ) {
$target_node = $parent_node->addChild( 'field' );
$target_node->addAttribute( 'key', (string) $key );
}
if ( isset( $sub_tree['__leaf__'] ) ) {
$info = $sub_tree['__leaf__'];
if ( ! isset( $target_node['type'] ) ) {
$target_node->addAttribute( 'type', $info['type'] ?? 'title' );
}
if ( ! isset( $target_node['editor_type'] ) ) {
$target_node->addAttribute( 'editor_type', $info['editor_type'] ?? 'LINE' );
}
}
$has_children = false;
foreach ( $sub_tree as $child_key => $child_val ) {
if ( '__leaf__' !== $child_key ) {
$has_children = true;
break;
}
}
if ( $has_children ) {
$child_tree = array_filter(
$sub_tree,
function ( $k ) {
return '__leaf__' !== $k;
},
ARRAY_FILTER_USE_KEY
);
$this->append_tree_to_simplexml( $target_node, $child_tree );
}
}
}
/**
* Generate snippet XML.
*
* @param string $name
* @param array $field_details
* @return string
*/
public function generate_snippet( string $name, array $field_details ): string {
$dom = new DOMDocument( '1.0', 'UTF-8' );
$dom->formatOutput = true;
$root = $dom->createElement( 'wpml-config' );
$dom->appendChild( $root );
$section = $dom->createElement( $this->section_name );
$widget = $dom->createElement( 'widget' );
$widget->setAttribute( 'name', $name );
$fields = $dom->createElement( 'fields' );
$tree = $this->build_fields_tree( $field_details );
$this->append_tree_to_dom( $dom, $fields, $tree );
$widget->appendChild( $fields );
$section->appendChild( $widget );
$root->appendChild( $section );
return $dom->saveXML();
}
/**
* Sync node.
*
* @param DOMElement $existing_widget
* @param SimpleXMLElement $new_widget
* @return void
*/
public function sync_node( DOMElement $existing_widget, SimpleXMLElement $new_widget ): void {
$owner_doc = $existing_widget->ownerDocument;
// Find or create element
$fields_elem = null;
$child_nodes = $existing_widget->childNodes;
for ( $i = 0; $i < $child_nodes->length; $i++ ) {
$child = $child_nodes->item( $i );
if ( XML_ELEMENT_NODE === $child->nodeType && 'fields' === $child->nodeName ) {
$fields_elem = $child;
break;
}
}
if ( ! $fields_elem ) {
$fields_elem = $existing_widget->appendChild( $owner_doc->createElement( 'fields' ) );
}
// Parse existing fields
$existing_fields_map = array();
$existing_no_translate_map = array();
$existing_fields_xml = simplexml_import_dom( $fields_elem );
if ( $existing_fields_xml && isset( $existing_fields_xml->field ) ) {
foreach ( $existing_fields_xml->field as $f_node ) {
$this->parse_field_node_recursive( $f_node, '', $existing_fields_map, $existing_no_translate_map );
}
}
// Extract fields map from new_widget
$new_fields_map = array();
$new_no_translate_map = array();
if ( isset( $new_widget->fields->field ) ) {
foreach ( $new_widget->fields->field as $f_node ) {
$this->parse_field_node_recursive( $f_node, '', $new_fields_map, $new_no_translate_map );
}
}
$final_specs = array();
// 1. Existing active fields
foreach ( $existing_fields_map as $path => $info ) {
if ( isset( $new_fields_map[ $path ] ) ) {
// Kept active with new settings
$final_specs[ $path ] = $new_fields_map[ $path ];
} elseif ( isset( $new_no_translate_map[ $path ] ) ) {
// Explicitly marked noTranslate
$spec = is_array( $new_no_translate_map[ $path ] ) ? $new_no_translate_map[ $path ] : ( is_array( $info ) ? $info : array(
'path' => $path,
'type' => 'title',
'editor_type' => 'LINE',
) );
$final_specs[ $path ] = array_merge( $spec, array( 'noTranslate' => true ) );
} else {
// First removal: mark as noTranslate="true" to preserve for auto-updating prevention
$spec = is_array( $info ) ? $info : array(
'path' => $path,
'type' => 'title',
'editor_type' => 'LINE',
);
$final_specs[ $path ] = array_merge( $spec, array( 'noTranslate' => true ) );
}
}
// 2. Existing noTranslate fields
foreach ( $existing_no_translate_map as $path => $info ) {
if ( isset( $new_fields_map[ $path ] ) ) {
// Restored to active
$final_specs[ $path ] = $new_fields_map[ $path ];
} elseif ( isset( $new_no_translate_map[ $path ] ) ) {
// Kept as noTranslate
$spec = is_array( $new_no_translate_map[ $path ] ) ? $new_no_translate_map[ $path ] : ( is_array( $info ) ? $info : array(
'path' => $path,
'type' => 'title',
'editor_type' => 'LINE',
) );
$final_specs[ $path ] = array_merge( $spec, array( 'noTranslate' => true ) );
} else {
// Second removal: already had noTranslate, user removed it again -> permanently delete it!
}
}
// 3. New active fields added
foreach ( $new_fields_map as $path => $info ) {
if ( ! isset( $existing_fields_map[ $path ] ) && ! isset( $existing_no_translate_map[ $path ] ) ) {
$final_specs[ $path ] = $info;
}
}
// 4. New explicit noTranslate fields added
foreach ( $new_no_translate_map as $path => $info ) {
if ( ! isset( $existing_fields_map[ $path ] ) && ! isset( $existing_no_translate_map[ $path ] ) ) {
$spec = is_array( $info ) ? $info : array(
'path' => $path,
'type' => 'title',
'editor_type' => 'LINE',
);
$final_specs[ $path ] = array_merge( $spec, array( 'noTranslate' => true ) );
}
}
// Rebuild fields DOM from final specs
while ( $fields_elem->firstChild ) {
$fields_elem->removeChild( $fields_elem->firstChild );
}
if ( ! empty( $final_specs ) ) {
$tree = $this->build_fields_tree( $final_specs );
$this->append_tree_to_dom( $owner_doc, $fields_elem, $tree );
}
// Sync conditions if present in new_widget
if ( isset( $new_widget->conditions ) ) {
for ( $i = 0; $i < $child_nodes->length; $i++ ) {
$child = $child_nodes->item( $i );
if ( XML_ELEMENT_NODE === $child->nodeType && 'conditions' === $child->nodeName ) {
$existing_widget->removeChild( $child );
break;
}
}
$cond_dom = dom_import_simplexml( $new_widget->conditions );
$existing_widget->insertBefore( $owner_doc->importNode( $cond_dom, true ), $fields_elem );
}
}
/**
* Append rule.
*
* @param SimpleXMLElement $xml
* @param string $name
* @param array $field_details
* @param array $conditions
* @return void
*/
public function append_rule( SimpleXMLElement $xml, string $name, array $field_details, array $conditions = array() ): void {
if ( ! isset( $xml->{$this->section_name} ) ) {
$xml->addChild( $this->section_name );
}
$found_widget = null;
foreach ( $xml->{$this->section_name}->widget as $wdg ) {
if ( (string) $wdg['name'] === $name ) {
$found_widget = $wdg;
break;
}
}
if ( ! $found_widget ) {
$found_widget = $xml->{$this->section_name}->addChild( 'widget' );
$found_widget->addAttribute( 'name', $name );
}
if ( ! isset( $found_widget->fields ) ) {
$found_widget->addChild( 'fields' );
}
$tree = $this->build_fields_tree( $field_details );
$this->append_tree_to_simplexml( $found_widget->fields, $tree );
}
/**
* Recursively parse XML nodes into path-mapped rule arrays.
*
* @param SimpleXMLElement $field_node
* @param string $prefix
* @param array $fields_out
* @param array $no_translate_out
*/
public function parse_field_node_recursive( SimpleXMLElement $field_node, string $prefix, array &$fields_out, array &$no_translate_out ): void {
$key = '';
if ( isset( $field_node['key'] ) ) {
$key = trim( (string) $field_node['key'] );
}
if ( empty( $key ) ) {
$key = trim( (string) $field_node );
}
$current_path = ( '' !== $prefix ) ? ( $prefix . '>' . $key ) : $key;
if ( isset( $field_node->field ) && count( $field_node->field ) > 0 ) {
foreach ( $field_node->field as $child_field ) {
$this->parse_field_node_recursive( $child_field, $current_path, $fields_out, $no_translate_out );
}
} else {
if ( empty( $current_path ) ) {
return;
}
$field_data = array(
'path' => $current_path,
'type' => (string) ( $field_node['type'] ?? 'title' ),
'editor_type' => (string) ( $field_node['editor_type'] ?? 'LINE' ),
);
$is_link_type = isset( $field_node['type'] ) && 'link' === strtolower( (string) $field_node['type'] );
$is_no_translate = $is_link_type || ( isset( $field_node['noTranslate'] ) && in_array( strtolower( (string) $field_node['noTranslate'] ), array( 'true', '1', 'yes' ), true ) );
if ( $is_no_translate ) {
if ( isset( $fields_out[ $current_path ] ) ) {
unset( $fields_out[ $current_path ] );
}
$no_translate_out[ $current_path ] = $field_data;
return;
}
$fields_out[ $current_path ] = $field_data;
}
}
/**
* Parse rules.
*
* @param SimpleXMLElement $xml
* @param array $rules
* @param string $source
* @return void
*/
public function parse_rules( SimpleXMLElement $xml, array &$rules, string $source = 'core' ): void {
if ( isset( $xml->{$this->section_name}->widget ) ) {
foreach ( $xml->{$this->section_name}->widget as $widget ) {
$name = (string) $widget['name'];
if ( ! isset( $rules['elementor'][ $name ] ) ) {
$rules['elementor'][ $name ] = array(
'name' => $name,
'source' => $source,
'conditions' => array(),
'fields' => array(),
);
}
if ( isset( $widget->conditions->condition ) ) {
foreach ( $widget->conditions->condition as $cond ) {
$rules['elementor'][ $name ]['conditions'][ (string) $cond['key'] ] = (string) $cond;
}
}
if ( isset( $widget->fields->field ) ) {
$no_translate = array();
foreach ( $widget->fields->field as $fld ) {
$this->parse_field_node_recursive( $fld, '', $rules['elementor'][ $name ]['fields'], $no_translate );
}
if ( ! empty( $no_translate ) ) {
$rules['elementor'][ $name ]['no_translate'] = $no_translate;
}
}
}
}
}
}