┌────────────────────────────────┐ │ ░░░ ░░▓ ░▓▓ ███ ▓▓░ ▓░░ │ │ ~ M O O N ~ │ └────────────────────────────────┘
prd-web-01.centraldc.gr
LOCATION:
/var/www/html/adconect/wp-content/plugins/smart-shortcode-v9-4
☗ ROOT
↻ REFRESH
✎ EDIT FILE
EDITING: smart-shortcode-v9-4.php
<?php /** * Plugin Name: Smart Shortcode Parser * Plugin URI: https://giannis.in * Description: Gutenberg block για εύκολη εισαγωγή και διαχείριση shortcodes. Αναλύει αυτόματα τα attributes κάθε registered shortcode μέσω PHP Reflection και Code Snippets DB fallback, εμφανίζει έξυπνα editor controls (Toggle, ColorPicker, ImagePicker, Textarea, NumberRange, SelectControl) βάσει τύπου τιμής ή type hints, και παρέχει εργαλείο Tester στο Tools menu με ιστορικό, copy-to-clipboard και live preview. * Version: 9.4 * Author: Giannis Giannopoulos / ai * License: GPL-2.0+ * * ── ΑΡΧΙΤΕΚΤΟΝΙΚΗ ──────────────────────────────────────────────────────────── * * 1. REFLECTION ENGINE (smart_shortcode_*) * Σαρώνει τα registered shortcodes μέσω PHP Reflection για να εξάγει τα * default attributes από κάθε shortcode_atts() call. Υποστηρίζει: * · Inline array: shortcode_atts( array('key' => 'val'), $atts ) * · Variable: $defaults = array(...); shortcode_atts( $defaults, $atts ) * · Keys με παύλες: 'background-image' => '' * · Code Snippets plugin: fallback μέσω wp_snippets table με σωστή * αντιστοίχιση tag → named function ή anonymous callback * Αποτέλεσμα: transient 'smart_shortcodes_map' (WEEK_IN_SECONDS). * Hints διατηρούνται κατά το resync. Hints ορφανών tags καθαρίζονται. * * 2. TYPE HINTS ($db['_hints'][tag][attr] = 'type') * * Α) INLINE HINTS (μέσα στον κώδικα του shortcode) — ΝΕΟΝ v9.2 * ───────────────────────────────────────────────────────────── * Προσθέτεις ένα comment μέσα στη function (ή στο snippet) με syntax: * * // @hints: hero-title="longtext"; background-image="image"; button-url="url" * * Παραδείγματα: * // @hints: background-image="image"; hero-title="longtext"; align="select:left,center,right" * // @hints: show_border="boolean"; repeat="number"; bg_color="color" * * Διαβάζεται αυτόματα κατά το Reflection scan (plugins) και κατά το * Code Snippets DB scan. Δεν χρειάζεται Tester — αρκεί ο comment. * Inline hints δεν αντικαθιστούν hints που ο χρήστης έχει ορίσει * χειροκίνητα μέσω του Tester (manual hints έχουν προτεραιότητα). * * Β) MANUAL HINTS (μέσω Tester) * ───────────────────────────── * Ο χρήστης ορίζει τύπο control ανά attribute μέσω του Tester. * Αποθηκεύονται ως $db['_hints'][tag][attr] = 'type'. * Έχουν προτεραιότητα έναντι inline hints. * * Υποστηριζόμενοι τύποι (και για τα δύο): * · image → MediaUpload (Image Picker) * · color → ColorPalette * · boolean → ToggleControl * · longtext → TextareaControl * · text / url → TextControl * · number → RangeControl (0–100) * · select:a,b,c → SelectControl με options a, b, c * * 3. GUTENBERG BLOCK (custom/smart-shortcode-parser) * Server-side rendered. Controls ανά attribute: * Ιεραρχία ανίχνευσης: manual hint > inline hint > auto-detect > TextControl * Auto-detect: boolean values → Toggle | #hex → Color | numeric → Number | * image extension → Image | length > 60 → Textarea | default → Text * Κάθε label εμφανίζει "attr (Default: τιμή)". * Shortcode string χτίζεται αυτόματα — παραλείπει attrs ίσες με default. * * 4. TOOLS PAGE (Tools → Shortcode Tester) * · Textarea: ένα shortcode ανά γραμμή * · Syntax: [tag key=hint key2="τιμή" key3=select:a,b,c] * · Πίνακας ανάλυσης: attribute / τιμή / πηγή / editor control * · Copy to clipboard για κάθε τελικό shortcode string * · Live preview μέσω do_shortcode() * · Ιστορικό τελευταίων 10 εκτελέσεων (user meta, χωρίς διπλότυπα) * · Διαγραφή μεμονωμένων εγγραφών από το ιστορικό * · Resync button με εμφάνιση remaining TTL * · Πίνακας όλων των registered shortcodes + hints (με πηγή: inline/manual) * * 5. INVALIDATION * Lazy init: χτίζεται μόνο αν δεν υπάρχει (admin_init). * Σβήνεται αυτόματα: cs_snippet_saved | activated/deactivated_plugin * Χειροκίνητα: κουμπί Resync στο Tester. */ if ( ! defined( 'ABSPATH' ) ) exit; // ══════════════════════════════════════════════════════════════════════════════ // 1. REFLECTION ENGINE // ══════════════════════════════════════════════════════════════════════════════ /** * Εξάγει inline hints από // @hints: comment μέσα σε function κώδικα. * Δουλεύει για plugins, functions.php και Code Snippets. * * Syntax: // @hints: key="type"; key2="select:a,b,c" * * @param string $code Κώδικας function ή snippet * @return array [ attr => type ] ή κενό array */ function smart_shortcode_extract_inline_hints( $code ) { // Υποστηρίζει και /* @hints: ... */ και // @hints: ... if ( ! preg_match( '/@hints:\s*(.+)$/m', $code, $hm ) ) return array(); $hints = array(); // Διαχωρισμός με ; αντί για , foreach ( array_map( 'trim', explode( ';', $hm[1] ) ) as $part ) { if ( strpos( $part, '=' ) !== false ) { list( $k, $v ) = explode( '=', $part, 2 ); $k = trim( $k ); // Αφαίρεση εισαγωγικών από την τιμή $v = trim( trim( trim( $v ), '"' ), "'" ); if ( $k !== '' && $v !== '' ) $hints[ $k ] = $v; } } return $hints; } /** * Σαρώνει όλα τα registered shortcodes μέσω PHP Reflection, * διατηρεί manual hints, εφαρμόζει inline hints (χαμηλότερη προτεραιότητα), * και αποθηκεύει στο transient. * * @return array $db [ tag => [ attr => default ], '_hints' => [...], '_hint_source' => [...] ] */ function smart_shortcode_sync_all_to_transient() { global $shortcode_tags; $existing = get_transient( 'smart_shortcodes_map' ); // Manual hints: ορίστηκαν από χρήστη μέσω Tester — έχουν προτεραιότητα $manual_hints = ( is_array($existing) && isset($existing['_manual_hints']) ) ? $existing['_manual_hints'] : array(); $db = array(); $inline_hints = array(); // hints από @hints comments foreach ( $shortcode_tags as $tag => $callback ) { if ( in_array( $tag, array('list_shortcodes','gallery','video','audio') ) ) continue; try { $reflection = null; if ( is_array($callback) ) { $reflection = new ReflectionMethod( $callback[0], $callback[1] ); } elseif ( $callback instanceof Closure || is_string($callback) ) { $reflection = new ReflectionFunction( $callback ); } if ( ! $reflection ) continue; $filename = $reflection->getFileName(); if ( ! $filename || ! file_exists($filename) || ! is_readable($filename) ) continue; $file = new SplFileObject( $filename ); $fn_code = ''; $file->seek( $reflection->getStartLine() - 1 ); while ( $file->key() < $reflection->getEndLine() ) { $fn_code .= $file->current(); $file->next(); } $pairs = smart_shortcode_extract_atts( $fn_code ); if ( $pairs !== null ) $db[$tag] = $pairs; // Inline hints από @hints comment $ih = smart_shortcode_extract_inline_hints( $fn_code ); if ( $ih ) $inline_hints[$tag] = $ih; } catch ( Exception $e ) { continue; } } // Fallback: Check Code Snippets DB if exists $db = smart_shortcode_scan_snippets_db( $db, $inline_hints ); // New: Check Custom Shortcodes Ultra DB $db = smart_shortcode_scan_custom_ultra_db( $db, $inline_hints ); // Καθάρισε manual hints για tags που δεν υπάρχουν πια foreach ( $manual_hints as $tag => $_ ) { if ( ! isset($db[$tag]) ) unset( $manual_hints[$tag] ); } // Merge hints: manual > inline // Το τελικό _hints που βλέπει ο editor είναι το merge των δύο $merged_hints = array(); $all_tags = array_unique( array_merge( array_keys($inline_hints), array_keys($manual_hints) ) ); foreach ( $all_tags as $tag ) { $il = $inline_hints[$tag] ?? array(); $mn = $manual_hints[$tag] ?? array(); // Inline πρώτα, manual overrides $merged_hints[$tag] = array_merge( $il, $mn ); } $db['_hints'] = $merged_hints; $db['_manual_hints'] = $manual_hints; // Αποθηκεύουμε χωριστά για διατήρηση μετά το resync $db['_inline_hints'] = $inline_hints; // Για εμφάνιση πηγής στον Tester set_transient( 'smart_shortcodes_map', $db, WEEK_IN_SECONDS ); return $db; } /** * Fallback για Code Snippets plugin. * Αντιστοιχεί κάθε add_shortcode() tag με το shortcode_atts() * της ίδιας function (named ή anonymous). * Εξάγει επίσης inline hints από @hints comments. * * @param array $db Υπάρχον db * @param array &$inline_hints Inline hints by reference για merge * @return array Εμπλουτισμένο db */ function smart_shortcode_scan_snippets_db( $db, &$inline_hints = array() ) { global $wpdb; $table = $wpdb->prefix . 'snippets'; if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return $db; $snippets = $wpdb->get_results("SELECT code FROM $table WHERE active = 1", ARRAY_A); foreach ( $snippets as $snippet ) { $code = $snippet['code']; // Named callbacks: add_shortcode('tag', 'function_name') preg_match_all( "/add_shortcode\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*['\"](\w+)['\"]\s*\)/", $code, $named, PREG_SET_ORDER ); foreach ( $named as $sc ) { $tag = $sc[1]; $fn_name = $sc[2]; // Εξαγωγή κώδικα της συγκεκριμένης function if ( preg_match('/function\s+' . preg_quote($fn_name,'/') . '\s*\([^)]*\)\s*\{.*?\}/ms', $code, $fn) ) { $fn_code = $fn[0]; } else { $fn_code = $code; // fallback: όλο το snippet } $pairs = smart_shortcode_extract_atts( $fn_code ); if ( $pairs !== null ) $db[$tag] = $pairs; // Inline hints από τη function $ih = smart_shortcode_extract_inline_hints( $fn_code ); if ( $ih ) $inline_hints[$tag] = array_merge( $inline_hints[$tag] ?? array(), $ih ); } // Anonymous callbacks: add_shortcode('tag', function($atts) { ... }) preg_match_all( "/add_shortcode\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*function\s*\([^)]*\)\s*(?:use\s*\([^)]*\)\s*)?\{(.*?)\}\s*\)/ms", $code, $anon, PREG_SET_ORDER ); foreach ( $anon as $sc ) { $tag = $sc[1]; $fn_code = $sc[0]; $pairs = smart_shortcode_extract_atts( $fn_code ); if ( $pairs !== null ) $db[$tag] = $pairs; $ih = smart_shortcode_extract_inline_hints( $fn_code ); if ( $ih ) $inline_hints[$tag] = array_merge( $inline_hints[$tag] ?? array(), $ih ); } } return $db; } /** * Εξάγει key=>default pairs από κώδικα function. * Υποστηρίζει inline array και variable assignment. * * @param string $code Κώδικας function * @return array|null Pairs ή null */ function smart_shortcode_extract_atts( $code ) { $pos = strpos($code, 'shortcode_atts'); if ( $pos === false ) return null; $open = strpos($code, '(', $pos); if ( $open === false ) return null; $depth = 0; $len = strlen($code); $first_arg = null; $inner_end = null; for ( $i=$open; $i<$len; $i++ ) { if ( $code[$i]==='(' ) { $depth++; if ($depth===1) $first_arg=$i+1; } elseif ( $code[$i]===')' ) { $depth--; if ($depth===0) { $inner_end=$i; break; } } } if ( $inner_end === null ) return null; $trimmed = ltrim( substr($code, $first_arg, $inner_end - $first_arg) ); // Περίπτωση Α: inline array if ( preg_match('/^(?:array\s*\(|\[)/i', $trimmed) ) { $content = smart_shortcode_extract_array_content($trimmed); return $content !== null ? smart_shortcode_parse_php_array($content) : null; } // Περίπτωση Β: variable if ( preg_match('/^\$(\w+)/', $trimmed, $m) ) { $var = preg_quote('$'.$m[1], '/'); if ( preg_match('/'.$var.'\s*=\s*(?:array\s*\(|\[)/i', $code, $vm, PREG_OFFSET_CAPTURE) ) { $content = smart_shortcode_extract_array_content( substr($code, $vm[0][1]) ); return $content !== null ? smart_shortcode_parse_php_array($content) : null; } } return null; } /** * Εξάγει το περιεχόμενο array(...) ή [...] μετρώντας brackets. * * @param string $code Κώδικας που ξεκινά με array( ή [ * @return string|null */ function smart_shortcode_extract_array_content( $code ) { $start=-1; $bt=null; for ($i=0; $i<strlen($code); $i++) { if ($code[$i]==='(' || $code[$i]==='[') { $start=$i; $bt=$code[$i]==='(' ? array('(', ')') : array('[', ']'); break; } } if ($start===-1) return null; $depth=0; $len=strlen($code); $end=null; for ($i=$start; $i<$len; $i++) { if ($code[$i]===$bt[0]) $depth++; elseif ($code[$i]===$bt[1]) { $depth--; if ($depth===0) { $end=$i; break; } } } return $end!==null ? substr($code,$start+1,$end-$start-1) : null; } /** * Καθαρίζει τιμή από comments και quotes. * * @param string $val Raw τιμή * @return string */ function smart_shortcode_clean_value( $val ) { $val = preg_replace('/\/\/.*$/', '', $val); if ( ! preg_match('/^["\']?\s*#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\s*["\']?$/', trim($val)) ) { $val = preg_replace('/#.*$/', '', $val); } return trim(trim(trim($val),'"'),"'"); } /** * Parses PHP array string σε associative array. * Υποστηρίζει keys με παύλες. * * @param string $string Περιεχόμενο array (χωρίς εξωτερικά brackets) * @return array */ function smart_shortcode_parse_php_array( $string ) { $pairs = array(); $parts = preg_split('/,(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/', $string); foreach ($parts as $part) { if (strpos($part,'=>')===false) continue; list($key,$val) = explode('=>',$part,2); $k = trim(trim(trim($key),'"'),"'"); $v = smart_shortcode_clean_value($val); if ($k!=='') $pairs[$k]=$v; } return $pairs; } // ── Invalidation ────────────────────────────────────────────────────────────── add_action('admin_init', function() { if ( false === get_transient('smart_shortcodes_map') ) smart_shortcode_sync_all_to_transient(); }); add_action('cs_snippet_saved', function() { delete_transient('smart_shortcodes_map'); }); add_action('activated_plugin', function() { delete_transient('smart_shortcodes_map'); }); add_action('deactivated_plugin', function() { delete_transient('smart_shortcodes_map'); }); /** * Σαρώνει τα δεδομένα από το Custom Shortcodes Ultra Plugin */ function smart_shortcode_scan_custom_ultra_db( $db, &$inline_hints = array() ) { $custom_data = get_option( 'custom_shortcodes_storage_final', [] ); if ( ! empty( $custom_data ) ) { // Φέρνουμε τα υπάρχοντα hints που έχουν ήδη σκαναριστεί από αρχεία $all_hints = get_option( 'smart_shortcodes_hints', array() ); foreach ( $custom_data as $sc ) { $tag = $sc['tag']; if ( empty( $tag ) ) continue; $pairs = []; if ( ! empty( $sc['attrs'] ) ) { foreach ( $sc['attrs'] as $attr ) { $pairs[ $attr['name'] ] = $attr['default']; // ΕΔΩ ΕΙΝΑΙ Η ΔΙΟΡΘΩΣΗ: Ενημερώνουμε το global hints option if ( ! empty( $attr['type'] ) ) { $all_hints[$tag][ $attr['name'] ] = $attr['type']; // Ενημερώνουμε και το inline variable για το τρέχον session $inline_hints[$tag][ $attr['name'] ] = $attr['type']; } } } $db[$tag] = $pairs; } // Αποθηκεύουμε τα νέα hints στη βάση για να τα βλέπει ο Gutenberg update_option( 'smart_shortcodes_hints', $all_hints ); } return $db; } // ══════════════════════════════════════════════════════════════════════════════ // 2. GUTENBERG BLOCK — ASSETS // ══════════════════════════════════════════════════════════════════════════════ add_action('enqueue_block_editor_assets', function() { global $shortcode_tags; $attr_db = get_transient('smart_shortcodes_map'); if ( ! is_array($attr_db) ) $attr_db = smart_shortcode_sync_all_to_transient(); wp_enqueue_media(); wp_register_script('smart-shortcode-js', '', array( 'wp-blocks','wp-element','wp-editor', 'wp-components','wp-server-side-render','wp-block-editor' )); wp_localize_script('smart-shortcode-js', 'SMART_DATA', array( 'tags' => array_keys($shortcode_tags), 'db' => $attr_db, 'hints' => isset($attr_db['_hints']) ? $attr_db['_hints'] : array(), )); wp_enqueue_script('smart-shortcode-js'); wp_add_inline_script('smart-shortcode-js', smart_shortcode_build_js()); }); /** * Επιστρέφει τον JS κώδικα του Gutenberg block. * * @return string JS */ function smart_shortcode_build_js() { return <<<'JS' ( function( blocks, element, blockEditor, components, serverSideRender ) { const { registerBlockType } = blocks; const { TextControl, PanelBody, TextareaControl, Placeholder, SelectControl, Button, ColorPalette, ToggleControl, RangeControl } = components; const { InspectorControls, useBlockProps, MediaUpload } = blockEditor; const { createElement: el, Fragment, useEffect } = element; // ── Helpers ─────────────────────────────────────────────────────────────── /** * Καθορίζει τον τύπο control. * Ιεραρχία: hint (manual > inline, merge στο PHP) > auto-detect > text */ function detectType( v, hint ) { const vl = String(v).toLowerCase(); const bools = ['true','false','on','off','yes','no']; if ( hint ) { if ( hint === 'boolean' ) return 'boolean'; if ( hint === 'image' ) return 'image'; if ( hint === 'color' ) return 'color'; if ( hint === 'longtext' ) return 'longtext'; if ( hint === 'number' ) return 'number'; if ( hint.startsWith('range:') ) return 'range'; if ( hint.startsWith('select:') ) return 'select'; if ( hint === 'url' || hint === 'text' ) return 'text'; } if ( bools.includes(vl) ) return 'boolean'; if ( vl.match(/\.(png|jpg|jpeg|gif|svg|webp)(\?.*)?$/i) ) return 'image'; if ( vl.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/) ) return 'color'; if ( v !== '' && !isNaN(v) ) return 'number'; if ( String(v).length > 60 ) return 'longtext'; return 'text'; } function getSelectOptions( hint ) { if ( !hint || !hint.startsWith('select:') ) return []; return hint.replace('select:','').split(',').map(v => ({ label:v.trim(), value:v.trim() })); } function getRangeValues( hint ) { if ( !hint || !hint.startsWith('range:') ) return { min: 0, max: 100 }; const parts = hint.replace('range:','').split(','); return { min: parts[0] !== undefined ? parseFloat(parts[0]) : 0, max: parts[1] !== undefined ? parseFloat(parts[1]) : 100 }; } /** Χτίζει shortcode string παραλείποντας attrs ίσες με default */ function buildShortcode( tag, map, db ) { let s = '[' + tag; for ( const k in map ) { const d = db[tag] && db[tag][k] !== undefined ? String(db[tag][k]) : ''; if ( String(map[k]) !== '' && String(map[k]) !== d ) s += ' ' + k + '="' + String(map[k]).replace(/"/g,'"') + '"'; } return s + ']'; } // ── Block ───────────────────────────────────────────────────────────────── registerBlockType('custom/smart-shortcode-parser', { title: 'Smart Shortcode', icon: el('svg', { width: 24, height: 24, viewBox: '0 0 20 20' }, el('rect', { width: 20.28, height: 16, x: -0.14, y: 2, fill: '#000000', opacity: 0.99 }), el('g', { fill: '#ffffff' }, el('path', { d: 'M6 14H4V6h2V4H2v12h4z' }), el('path', { d: 'M7.1 17h2.1l3.7-14h-2.1z' }), el('path', { d: 'M14 4v2h2v8h-2v2h4V4z' }) ) ), category: 'widgets', attributes: { shortcode: { type: 'string', default: '' }, attributes_map: { type: 'object', default: {} }, selected_tag: { type: 'string', default: 'custom' } }, edit: function( props ) { const { attributes, setAttributes } = props; const { shortcode='', attributes_map={}, selected_tag='custom' } = attributes; const C = window.SMART_DATA || { tags:[], db:{}, hints:{} }; const options = [{ label:'--- Custom / Manual ---', value:'custom' }]; C.tags.slice().sort().forEach(t => options.push({ label:'['+t+']', value:t })); const nameMatch = shortcode.match(/\[([^\s\]]+)/); const curTag = nameMatch ? nameMatch[1] : ''; // Sync map από shortcode string useEffect(() => { if ( !curTag ) return; const defs = C.db[curTag] || {}; let m = { ...defs }; const re = /([\w-]+)\s*=\s*"([^"]*)"/g; let match; while ( (match=re.exec(shortcode)) !== null ) m[match[1]] = match[2]; if ( JSON.stringify(m) !== JSON.stringify(attributes_map) ) setAttributes({ attributes_map: m }); }, [shortcode, curTag]); const updateAttr = (k, v) => { const m = { ...attributes_map, [k]: String(v) }; setAttributes({ attributes_map:m, shortcode:buildShortcode(curTag||'shortcode',m,C.db) }); }; // ── Render control ──────────────────────────────────────────────── const renderCtrl = (k) => { const v = String(attributes_map[k] !== undefined ? attributes_map[k] : ''); const vl = v.toLowerCase(); const H = (C.hints && C.hints[curTag]) ? C.hints[curTag] : {}; const hint = H[k] || null; const type = detectType(v, hint); const defs = C.db[curTag] || {}; const dVal = defs[k] !== undefined ? String(defs[k]) : ''; const label = k + (dVal !== '' ? ' (Default: '+dVal+')' : ''); switch (type) { case 'boolean': const chk = ['true','on','yes'].includes(vl); return el(ToggleControl, { key:k, label, checked:chk, onChange:(val)=>{ let next=val; if (vl==='on'||vl==='off') next=val?'on':'off'; else if (vl==='yes'||vl==='no') next=val?'yes':'no'; else next=val?'true':'false'; updateAttr(k,next); }}); case 'image': return el('div',{style:{marginBottom:'15px'},key:k}, el('span',{style:{fontSize:'11px',fontWeight:'600',display:'block',marginBottom:'5px'}},label), el(MediaUpload,{onSelect:(m)=>updateAttr(k,m.url),allowedTypes:['image'], render:({open})=>el(Button,{isSecondary:true,onClick:open},'Select Image')}), v && el('div',{style:{marginTop:'5px',fontSize:'10px',color:'#888',wordBreak:'break-all'}},v) ); case 'color': return el('div',{style:{marginBottom:'15px'},key:k}, el('span',{style:{fontSize:'11px',fontWeight:'600',display:'block',marginBottom:'5px'}},label), el(ColorPalette,{value:v,onChange:(c)=>updateAttr(k,c||'')}) ); case 'number': return el(TextControl,{key:k,label,value:v,type:'number', onChange:(val)=>updateAttr(k,val)}); case 'range': const range = getRangeValues(hint); return el(RangeControl,{key:k,label,value:parseFloat(v)||0, min:range.min,max:range.max,step:1, onChange:(val)=>updateAttr(k,String(val))}); case 'select': return el(SelectControl,{key:k,label,value:v, options:getSelectOptions(hint),onChange:(val)=>updateAttr(k,val)}); case 'longtext': return el(TextareaControl,{key:k,label,value:v,onChange:(val)=>updateAttr(k,val)}); default: return el(TextControl,{key:k,label,value:v,onChange:(val)=>updateAttr(k,val)}); } }; // ── Block output ────────────────────────────────────────────────── return el('div', useBlockProps(), el(Fragment,{}, el(InspectorControls,{}, el(PanelBody,{title:'Shortcode Attributes',initialOpen:true}, el(SelectControl,{label:'Shortcode',value:selected_tag,options, onChange:(v)=>{ setAttributes({selected_tag:v}); if(v!=='custom') setAttributes({shortcode:'['+v+']',attributes_map:{}}); }}), el(TextareaControl,{label:'Raw Shortcode',value:shortcode, onChange:(v)=>setAttributes({shortcode:v})}), Object.keys(attributes_map).length>0 && el('hr'), Object.keys(attributes_map).map(renderCtrl) ) ), el('div',{style:{border:'1px solid #ddd',borderRadius:'3px',overflow:'hidden',minHeight:'60px'}}, el('div',{style:{background:'#1e1e1e',color:'#7ec8e3',padding:'6px 12px',fontSize:'12px',fontFamily:'monospace'}}, '⚡ '+(curTag?'['+curTag+']':'Smart Shortcode — επιλέξτε shortcode')), shortcode.trim()==='' ? el(Placeholder,{label:'Επιλέξτε shortcode από το sidebar',icon:'admin-generic'}) : el(serverSideRender,{block:'custom/smart-shortcode-parser',attributes}) ) ) ); }, save: () => null }); })( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.components, window.wp.serverSideRender ); JS; } // ══════════════════════════════════════════════════════════════════════════════ // 3. BLOCK REGISTRATION & SERVER-SIDE RENDER // ══════════════════════════════════════════════════════════════════════════════ add_action('init', function() { register_block_type('custom/smart-shortcode-parser', array( 'api_version' => 2, 'attributes' => array( 'shortcode' => array('type'=>'string','default'=>''), 'attributes_map' => array('type'=>'object','default'=>array()), 'selected_tag' => array('type'=>'string','default'=>'custom'), ), 'render_callback' => function($attributes) { if ( empty($attributes['shortcode']) ) return ''; //return '<div class="smart-shortcode-output">'.do_shortcode($attributes['shortcode']).'</div>'; return do_shortcode($attributes['shortcode']); }, )); }); // ══════════════════════════════════════════════════════════════════════════════ // 4. TOOLS PAGE — SHORTCODE TESTER // ══════════════════════════════════════════════════════════════════════════════ add_action('admin_menu', function() { add_management_page('Shortcode Tester','Shortcode Tester','manage_options', 'smart-shortcode-tester','smart_shortcode_tester_page'); }); /** * Parses μια γραμμή shortcode εισόδου. * Αποθηκεύει manual hints στο $attr_db (by reference). * * @param string $line Γραμμή εισόδου * @param array &$attr_db Transient data by reference * @return array|null */ function smart_shortcode_tester_parse_line( $line, &$attr_db ) { $scalar_hints = array('image','longtext','text','color','boolean','number','url','range'); if ( ! preg_match('/\[(\w[\w-]*)/', $line, $tm) ) return null; $tag = $tm[1]; $given = array(); preg_match_all('/([\w-]+)\s*=\s*(?:"([^"]*)"|([\S]+))/', $line, $am, PREG_SET_ORDER); foreach ($am as $m) { $given[$m[1]] = (isset($m[3])&&$m[3]!=='') ? $m[3] : $m[2]; } if ( ! isset($attr_db['_manual_hints']) ) $attr_db['_manual_hints'] = array(); $defaults = isset($attr_db[$tag]) ? $attr_db[$tag] : array(); $final_atts = $defaults; foreach ($given as $k => $v) { $v_lower = strtolower($v); $is_scalar = in_array($v_lower, $scalar_hints) || strpos($v_lower,':')!==false; $is_select = strpos($v_lower,'select:')===0; if ( $is_scalar || $is_select ) { // Manual hint — αποθηκεύεται χωριστά, override inline hints $attr_db['_manual_hints'][$tag][$k] = strtolower($v); if ( ! array_key_exists($k,$final_atts) ) $final_atts[$k]=''; } else { $final_atts[$k] = $v; } if ( ! isset($attr_db[$tag][$k]) ) $attr_db[$tag][$k]=''; } // Rebuild merged hints για αυτό το tag $inline = $attr_db['_inline_hints'][$tag] ?? array(); $manual = $attr_db['_manual_hints'][$tag] ?? array(); $attr_db['_hints'][$tag] = array_merge($inline, $manual); $final_sc = '['.$tag; foreach ($final_atts as $k=>$v) { if ($v!=='') $final_sc.=' '.$k.'="'.esc_attr($v).'"'; } $final_sc.=']'; return array( 'tag' => $tag, 'given' => $given, 'final_atts' => $final_atts, 'final_sc' => $final_sc, 'preview' => do_shortcode($final_sc), 'hints' => $attr_db['_hints'][$tag] ?? array(), ); } /** * Αποθηκεύει input στο ιστορικό χρήστη (user meta). * Μέγιστο 10 εγγραφές, χωρίς διπλότυπα. * * @param string $input */ function smart_shortcode_save_history( $input ) { $uid = get_current_user_id(); if ( !$uid ) return; $h = get_user_meta($uid,'sst_history',true); if ( !is_array($h) ) $h=array(); $h = array_values(array_filter($h,fn($e)=>trim($e)!==trim($input))); array_unshift($h,$input); update_user_meta($uid,'sst_history',array_slice($h,0,10)); } /** * Διαγραφή μεμονωμένης εγγραφής από το ιστορικό. * * @param int $index */ function smart_shortcode_delete_history_item( $index ) { $uid = get_current_user_id(); if ( !$uid ) return; $h = get_user_meta($uid,'sst_history',true); if ( !is_array($h) ) return; if ( isset($h[$index]) ) { unset($h[$index]); $h = array_values($h); // re-index update_user_meta($uid,'sst_history',$h); } } /** * Render της Tools page. */ function smart_shortcode_tester_page() { $attr_db = get_transient('smart_shortcodes_map'); if ( !is_array($attr_db) ) $attr_db = smart_shortcode_sync_all_to_transient(); $scalar_hints = array('image','longtext','text','color','boolean','number','url'); $input=''; $results=array(); $message=''; // Resync if ( isset($_POST['sst_resync']) && check_admin_referer('sst_resync') ) { delete_transient('smart_shortcodes_map'); $attr_db = smart_shortcode_sync_all_to_transient(); $message = '✅ Resync ολοκληρώθηκε. Βρέθηκαν '.(count($attr_db)-3).' shortcodes.'; } // Διαγραφή μεμονωμένης εγγραφής από το ιστορικό if ( isset($_GET['sst_delete_history']) && is_numeric($_GET['sst_delete_history']) && check_admin_referer('sst_delete_history') ) { smart_shortcode_delete_history_item( (int)$_GET['sst_delete_history'] ); $message = '🗑 Η εγγραφή διαγράφηκε από το ιστορικό.'; } // Load από history if ( isset($_GET['sst_history']) && is_numeric($_GET['sst_history']) ) { $h = get_user_meta(get_current_user_id(),'sst_history',true); $i = (int)$_GET['sst_history']; if (isset($h[$i])) $input=$h[$i]; } // Run tester if ( !empty($_POST['sc_input']) && check_admin_referer('sst_test') ) { $input = sanitize_textarea_field(wp_unslash($_POST['sc_input'])); $lines = array_filter(array_map('trim',explode("\n",$input))); foreach ($lines as $line) { $r = smart_shortcode_tester_parse_line($line,$attr_db); if ($r) $results[]=$r; } if ($results) { smart_shortcode_save_history($input); set_transient('smart_shortcodes_map',$attr_db,WEEK_IN_SECONDS); } } $history = get_user_meta(get_current_user_id(),'sst_history',true); if ( !is_array($history) ) $history=array(); $known_count = count(array_filter(array_keys($attr_db),fn($k)=>!in_array($k,array('_hints','_manual_hints','_inline_hints')))); $ttl_opt = get_option('_transient_timeout_smart_shortcodes_map'); $ttl_str = $ttl_opt ? human_time_diff(time(),$ttl_opt).' απομένουν' : 'N/A'; ?> <style> #sst { max-width:980px; padding:28px 0; font-family:sans-serif; color:#1e1e1e; } #sst h1 { font-size:22px; font-weight:800; margin:0 0 6px; } #sst .sub { color:#666; font-size:13px; margin-bottom:28px; } #sst .card { background:#fff; border:1px solid #e0e0e0; border-radius:8px; padding:22px 24px; margin-bottom:20px; } #sst .card-title { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:#0073aa; margin:0 0 14px; } #sst label { display:block; font-size:12px; font-weight:600; color:#444; margin-bottom:7px; } #sst textarea { width:100%; box-sizing:border-box; font-family:'Courier New',monospace; font-size:13px; line-height:1.9; padding:10px 13px; border:1px solid #ccc; border-radius:5px; resize:vertical; min-height:100px; } #sst textarea:focus { outline:none; border-color:#0073aa; box-shadow:0 0 0 2px rgba(0,115,170,.15); } #sst .hint-box { margin-top:12px; padding:11px 15px; background:#f0f7fb; border-left:3px solid #0073aa; border-radius:0 5px 5px 0; font-size:12px; color:#555; line-height:1.8; } #sst .hint-box code { background:#dceef7; color:#0073aa; padding:1px 5px; border-radius:3px; } #sst .inline-hint-box { margin-top:12px; padding:11px 15px; background:#f3faf3; border-left:3px solid #46a35e; border-radius:0 5px 5px 0; font-size:12px; color:#2d5a35; line-height:1.8; } #sst .inline-hint-box code { background:#d6f0db; color:#2d5a35; padding:1px 5px; border-radius:3px; } #sst .btn { display:inline-block; padding:9px 22px; border-radius:5px; font-size:13px; font-weight:600; cursor:pointer; border:none; line-height:1; transition:background .15s; } #sst .btn-primary { background:#0073aa; color:#fff; } #sst .btn-primary:hover { background:#005a87; } #sst .btn-secondary { background:#f0f0f0; color:#333; border:1px solid #ccc; } #sst .btn-secondary:hover { background:#e0e0e0; } #sst .btn-danger { background:#dc3545; color:#fff; } #sst .btn-danger:hover { background:#c82333; } #sst .btn-small { padding:4px 10px; font-size:11px; } #sst .btns { display:flex; gap:10px; margin-top:14px; align-items:center; } #sst .msg { padding:10px 16px; border-radius:5px; background:#ecf7ed; border:1px solid #b2dfb4; color:#2e7d32; font-size:13px; margin-bottom:16px; } #sst .result-block { border:2px solid #e0e0e0; border-radius:8px; margin-bottom:22px; overflow:hidden; } #sst .result-header { background:#1e1e1e; color:#fff; padding:10px 18px; display:flex; align-items:center; gap:10px; } #sst .result-header .rtag { background:#0073aa; color:#fff; padding:3px 12px; border-radius:20px; font-size:12px; font-weight:700; font-family:monospace; } #sst .result-header .rnum { color:#888; font-size:12px; } #sst .result-body { padding:20px 24px; } #sst .sc-box { background:#1e1e1e; color:#7ec8e3; font-family:'Courier New',monospace; font-size:13px; padding:12px 16px; border-radius:5px; word-break:break-all; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } #sst .copy-btn { background:#333; color:#aaa; border:1px solid #555; border-radius:4px; padding:4px 10px; font-size:11px; cursor:pointer; white-space:nowrap; flex-shrink:0; } #sst .copy-btn:hover { background:#444; color:#fff; } #sst .preview-box { border:1px solid #e8e8e8; border-radius:5px; padding:18px 20px; background:#fafafa; margin-top:16px; } #sst table { width:100%; border-collapse:collapse; font-size:13px; } #sst th { text-align:left; padding:8px 12px; background:#f5f5f5; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#666; border-bottom:1px solid #e0e0e0; } #sst td { padding:8px 12px; border-bottom:1px solid #f2f2f2; vertical-align:middle; } #sst tr:last-child td { border-bottom:none; } .badge { display:inline-block; padding:2px 9px; border-radius:20px; font-size:11px; font-weight:600; } .b-hint { background:#fff3cd; color:#856404; } .b-inline { background:#d6f0db; color:#2d5a35; } .b-user { background:#d4edda; color:#155724; } .b-def { background:#f0f0f0; color:#555; } .hchip { display:inline-block; padding:2px 8px; border-radius:20px; font-size:11px; font-weight:600; margin:2px 3px 2px 0; font-family:monospace; } .hchip-manual { background:#fff3cd; color:#856404; } .hchip-inline { background:#d6f0db; color:#2d5a35; } #sst .history-list { display:flex; flex-direction:column; gap:6px; } #sst .history-item { display:flex; align-items:center; gap:8px; } #sst .history-item a { display:block; font-family:monospace; font-size:12px; color:#0073aa; text-decoration:none; background:#f5f5f5; padding:5px 10px; border-radius:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; } #sst .history-item a:hover { background:#e8f4fb; } #sst .history-delete { color:#dc3545; text-decoration:none; font-size:16px; font-weight:bold; padding:2px 6px; border-radius:4px; flex-shrink:0; } #sst .history-delete:hover { background:#fce4e7; } #sst hr { border:none; border-top:1px solid #f0f0f0; margin:16px 0; } #sst .two-col { display:grid; grid-template-columns:1fr 1fr; gap:20px; } #sst .meta-info { font-size:11px; color:#aaa; margin:8px 0 0; } @media(max-width:768px){ #sst .two-col { grid-template-columns:1fr; } } </style> <div id="sst"> <h1>🔧 Shortcode Tester</h1> <p class="sub">Ορισμός attributes, type hints και preview shortcodes. Ένα shortcode ανά γραμμή.</p> <?php if ($message): ?><div class="msg"><?php echo esc_html($message); ?></div><?php endif; ?> <div class="two-col"> <!-- Input --> <div class="card"> <p class="card-title">Εισαγωγή Shortcodes</p> <form method="post"> <?php wp_nonce_field('sst_test'); ?> <label>Ένα shortcode ανά γραμμή</label> <textarea name="sc_input" placeholder="[hero_slide hero-title=longtext background-image=image] [demo_box bg_color=color show_border=boolean repeat=number] [my_sc align=select:left,center,right title="Τίτλος"]"><?php echo esc_textarea($input); ?></textarea> <div class="hint-box"> <strong>Manual type hints</strong> — γράψε αντί για τιμή:<br> <code>image</code> · <code>color</code> · <code>boolean</code> · <code>longtext</code> · <code>text</code> · <code>url</code> · <code>number</code> · <code>range:min,max</code> · <code>select:a,b,c</code><br> Πραγματικές τιμές: <code>key="τιμή"</code> ή <code>key=τιμή</code>. </div> <div class="inline-hint-box"> <strong>Inline hints (v9.2)</strong> — γράψε μέσα στον κώδικα του shortcode:<br> <code>// @hints: hero-title="longtext"; background-image="image"; button-url="url"; repeat="range:1,10"</code><br> Διαβάζεται αυτόματα από plugins <em>και</em> Code Snippets κατά το Resync. </div> <div class="btns"> <button type="submit" class="btn btn-primary">▶ Εκτέλεση</button> </div> </form> </div> <!-- History + Tools --> <div class="card"> <p class="card-title">Ιστορικό (<?php echo count($history); ?>/10)</p> <?php if ($history): ?> <div class="history-list"> <?php foreach ($history as $idx=>$h): $delete_url = wp_nonce_url( add_query_arg(array( 'page' => 'smart-shortcode-tester', 'sst_delete_history' => $idx ), admin_url('tools.php')), 'sst_delete_history' ); ?> <div class="history-item"> <a href="<?php echo esc_url(add_query_arg(array('page'=>'smart-shortcode-tester','sst_history'=>$idx),admin_url('tools.php'))); ?>" title="<?php echo esc_attr($h); ?>"> <?php echo esc_html(mb_strimwidth($h,0,80,'…')); ?> </a> <a href="<?php echo esc_url($delete_url); ?>" class="history-delete" title="Διαγραφή αυτής της εγγραφής" onclick="return confirm('Διαγραφή αυτής της εγγραφής από το ιστορικό;');">×</a> </div> <?php endforeach; ?> </div> <?php else: ?> <p style="color:#aaa;font-size:13px;margin:0;">Δεν υπάρχει ιστορικό ακόμα.</p> <?php endif; ?> <hr> <p class="card-title" style="margin-bottom:10px;">Εργαλεία</p> <form method="post" style="display:inline;"> <?php wp_nonce_field('sst_resync'); ?> <button type="submit" name="sst_resync" value="1" class="btn btn-secondary">🔄 Resync Shortcodes</button> </form> <p class="meta-info"> Shortcodes: <strong><?php echo $known_count; ?></strong> · Transient: <?php echo esc_html($ttl_str); ?> </p> </div> </div> <!-- Results --> <?php foreach ($results as $idx=>$r): ?> <div class="result-block"> <div class="result-header"> <span class="rtag">[<?php echo esc_html($r['tag']); ?>]</span> <span class="rnum">Shortcode #<?php echo $idx+1; ?></span> </div> <div class="result-body"> <p class="card-title">Τελικό Shortcode</p> <div class="sc-box"> <span class="sc-text"><?php echo esc_html($r['final_sc']); ?></span> <button class="copy-btn" onclick=" navigator.clipboard.writeText(this.closest('.sc-box').querySelector('.sc-text').textContent); this.textContent='✓ Copied!'; setTimeout(()=>this.textContent='Copy',1500);">Copy</button> </div> <hr> <p class="card-title">Ανάλυση Attributes</p> <table> <thead><tr><th>Attribute</th><th>Τιμή</th><th>Πηγή</th><th>Editor Control</th></tr></thead> <tbody> <?php foreach ($r['final_atts'] as $k=>$v): $gr = $r['given'][$k] ?? null; $is_hint = $gr && (in_array(strtolower($gr),$scalar_hints)||strpos($gr,'select:')===0); $src = $gr ? ($is_hint?'hint → default':'χρήστης') : 'default'; $bdg = $gr ? ($is_hint?'b-hint':'b-user') : 'b-def'; $hk = $r['hints'][$k] ?? null; $vl = strtolower($v); if ($hk==='image' || preg_match('/\.(png|jpg|jpeg|gif|svg|webp)(\?.*)?$/i',$v)) $ctrl='Image Picker'; elseif ($hk==='color' || preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i',$v)) $ctrl='Color Picker'; elseif ($hk==='boolean' || in_array($vl,['true','false','on','off','yes','no'])) $ctrl='Toggle'; elseif ($hk==='number' || ($v!==''&&is_numeric($v))) $ctrl='Number (Range)'; elseif ($hk&&strpos($hk,'select:')===0) $ctrl='Select ('.str_replace('select:','',$hk).')'; elseif ($hk==='longtext'||strlen($v)>60) $ctrl='Textarea'; elseif ($hk) $ctrl='Text Input ('.$hk.')'; else $ctrl='Text Input'; ?> <tr> <td><code><?php echo esc_html($k); ?></code></td> <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?php echo esc_attr($v); ?>"> <?php echo esc_html($v!==''?$v:'(κενό)'); ?> </td> <td><span class="badge <?php echo $bdg; ?>"><?php echo esc_html($src); ?></span></td> <td><?php echo esc_html($ctrl); ?></td> </tr> <?php endforeach; ?> </tbody> </table> <hr> <p class="card-title">Preview</p> <div class="preview-box"><?php echo $r['preview']; ?></div> </div> </div> <?php endforeach; ?> <?php if ($input&&empty($results)): ?> <div class="card" style="color:#c0392b;">⚠ Δεν βρέθηκε κανένα έγκυρο shortcode tag.</div> <?php endif; ?> <!-- Registered shortcodes --> <div class="card"> <p class="card-title">Registered Shortcodes (<?php echo $known_count; ?>)</p> <table> <thead><tr><th>Tag</th><th>Attributes</th><th>Hints</th></tr></thead> <tbody> <?php $sys_keys = array('_hints','_manual_hints','_inline_hints'); foreach ($attr_db as $t=>$atts): if (in_array($t,$sys_keys)) continue; $manual_h = $attr_db['_manual_hints'][$t] ?? array(); $inline_h = $attr_db['_inline_hints'][$t] ?? array(); // Όλα τα unique hint keys $all_hint_keys = array_unique(array_merge(array_keys($inline_h),array_keys($manual_h))); ?> <tr> <td><code>[<?php echo esc_html($t); ?>]</code></td> <td style="font-size:12px;color:#444;"><?php echo esc_html(implode(', ',array_keys($atts))); ?></td> <td> <?php foreach ($all_hint_keys as $hk): // Manual override inline — δείξε την αποτελεσματική τιμή με χρωματική ένδειξη πηγής if (isset($manual_h[$hk])): echo '<span class="hchip hchip-manual" title="manual (Tester)">'.esc_html($hk.'='.$manual_h[$hk]).'</span>'; else: echo '<span class="hchip hchip-inline" title="inline (@hints comment)">'.esc_html($hk.'='.$inline_h[$hk]).'</span>'; endif; endforeach; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <p style="font-size:11px;color:#888;margin:12px 0 0;"> <span style="display:inline-block;background:#fff3cd;color:#856404;padding:1px 8px;border-radius:20px;font-size:11px;">κίτρινο</span> = manual (Tester) · <span style="display:inline-block;background:#d6f0db;color:#2d5a35;padding:1px 8px;border-radius:20px;font-size:11px;">πράσινο</span> = inline (@hints comment) </p> </div> </div> <?php } // ── Plugin action links ────────────────────────────────────────────────────── add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) { $settings_link = '<a href="' . esc_url(admin_url('tools.php?page=smart-shortcode-tester')) . '">Settings</a>'; array_unshift($links, $settings_link); return $links; }); // ══════════════════════════════════════════════════════════════════════════════ // 5. DEMO SHORTCODE // ══════════════════════════════════════════════════════════════════════════════ add_shortcode('demo_box', function($atts) { // @hints: image="image"; bg_color="color"; show_border="boolean"; repeat="range:1,10"; long_text="longtext" $a = shortcode_atts(array( 'image' => 'demo.png', 'bg_color' => '#eeeeee', 'show_border' => 'yes', 'small_text' => 'Just a title', 'long_text' => 'This is a very long text designed to trigger the textarea control in our smart parser because it exceeds sixty characters.', 'repeat' => '1', ), $atts); $border = in_array($a['show_border'],array('yes','true','on')) ? '2px solid #333' : 'none'; $out=''; for ($i=0;$i<intval($a['repeat']);$i++) { $out.='<div style="background:'.esc_attr($a['bg_color']).';border:'.$border.';padding:15px;margin-bottom:5px;overflow:hidden;display:flex;align-items:center;gap:15px;">'; $out.='<img src="'.esc_url($a['image']).'" style="width:50px;height:50px;object-fit:cover;">'; $out.='<div><strong>'.esc_html($a['small_text']).'</strong>'; $out.='<p style="margin:0;font-size:13px;">'.esc_html($a['long_text']).'</p></div></div>'; } return $out; });
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📄 smart-shortcode-v9-4.php
PHP
56 KB
2026-05-19 10:28
EDIT