abc_myplugin-0.1.txt // Plugin name is optional. If unset, it will be extracted from the current // file name. Plugin names should start with a three letter prefix which is // unique and reserved for each plugin author ("abc" is just an example). // Uncomment and edit this line to override: $plugin['name'] = 'smd_article_stats'; // Allow raw HTML help, as opposed to Textile. // 0 = Plugin help is in Textile format, no raw HTML allowed (default). // 1 = Plugin help is in raw HTML. Not recommended. # $plugin['allow_html_help'] = 1; $plugin['version'] = '0.5.1'; $plugin['author'] = 'Stef Dawson'; $plugin['author_uri'] = 'https://stefdawson.com/'; $plugin['description'] = 'Get article/excerpt statistics and display them to content editors and visitors'; // Plugin load order: // The default value of 5 would fit most plugins, while for instance comment // spam evaluators or URL redirectors would probably want to run earlier // (1...4) to prepare the environment for everything else that follows. // Values 6...9 should be considered for plugins which would work late. // This order is user-overrideable. $plugin['order'] = '5'; // Plugin 'type' defines where the plugin is loaded // 0 = public : only on the public side of the website (default) // 1 = public+admin : on both the public and admin side // 2 = library : only when include_plugin() or require_plugin() is called // 3 = admin : only on the admin side (no AJAX) // 4 = admin+ajax : only on the admin side (AJAX supported) // 5 = public+admin+ajax : on both the public and admin side (AJAX supported) $plugin['type'] = '5'; // Plugin "flags" signal the presence of optional capabilities to the core plugin loader. // Use an appropriately OR-ed combination of these flags. // The four high-order bits 0xf000 are available for this plugin's private use if (!defined('PLUGIN_HAS_PREFS')) define('PLUGIN_HAS_PREFS', 0x0001); // This plugin wants to receive "plugin_prefs.{$plugin['name']}" events if (!defined('PLUGIN_LIFECYCLE_NOTIFY')) define('PLUGIN_LIFECYCLE_NOTIFY', 0x0002); // This plugin wants to receive "plugin_lifecycle.{$plugin['name']}" events $plugin['flags'] = '1'; // Plugin 'textpack' is optional. It provides i18n strings to be used in conjunction with gTxt(). // Syntax: // ## arbitrary comment // #@event // #@language ISO-LANGUAGE-CODE // abc_string_name => Localized String $plugin['textpack'] = << Article statistics smd_artstat_char_plural => chars smd_artstat_char_singular => char smd_artstat_fields => Word count fields and DOM selectors smd_artstat_id => Show article ID smd_artstat_legend => Article stats smd_artstat_pos => Position of stats panel smd_artstat_pos_above_sort_display => Above Sort and display smd_artstat_pos_above_title => Above Title smd_artstat_pos_below_author => Below Author smd_artstat_pos_below_excerpt => Below Excerpt smd_artstat_pos_below_sort_display => Below Sort and display smd_artstat_set_by_admin => Set by administrator smd_artstat_show_char => Show character count smd_artstat_show_word => Show word count smd_artstat_singular => Numbers treated as 'singular' smd_artstat_word_plural => words smd_artstat_word_singular => word #@language fr-fr smd_artstat => Statistiques d'article smd_artstat_char_plural => caractères smd_artstat_char_singular => caractère smd_artstat_fields => Champs de décompte et sélecteur du DOM smd_artstat_id => Afficher l'ID de l'article smd_artstat_legend => Statistiques article smd_artstat_pos => Emplacement dans la page smd_artstat_pos_above_sort_display => Au dessus l'article tri et affichage smd_artstat_pos_above_title => Au dessus du titre smd_artstat_pos_below_author => Sous le nom d'auteur smd_artstat_pos_below_excerpt => Sous le résumé smd_artstat_pos_below_sort_display => Sous l'article tri et affichage smd_artstat_set_by_admin => Définie par l'administrateur smd_artstat_show_char => Afficher le nombre de caractères smd_artstat_show_word => Afficher le compte de mots smd_artstat_singular => Nombre pris au singulier smd_artstat_word_plural => mots smd_artstat_word_singular => mot EOT; if (!defined('txpinterface')) @include_once('zem_tpl.php'); # --- BEGIN PLUGIN CODE --- /** * smd_article_stats * * A Textpattern CMS plugin for counting words/characters in article fields and * optionally displaying them to visitors. * -> Choose which fields to count on the admin side. * -> Customize where you want the count to be displayed. * -> Shows ID of currently edited article. * * @author Stef Dawson * @link https://stefdawson.com/ * @todo TinyMCE -- accessing fields inside iframes? */ if (txpinterface === 'admin') { $all_privs = array_keys(get_groups()); unset($all_privs[array_search('0', $all_privs)]); // Remove 'none'. $all_joined = implode(',', $all_privs); add_privs('smd_artstat_prefs', $all_joined); add_privs('prefs.smd_artstat', $all_joined); add_privs('plugin_prefs.smd_article_stats', $all_joined); register_callback('smd_artstat_prefs', 'prefs', '', 1); register_callback('smd_article_info', 'article'); register_callback('smd_artstat_options', 'plugin_prefs.smd_article_stats', null, 1); } elseif (txpinterface === 'public') { if (class_exists('\Textpattern\Tag\Registry')) { Txp::get('\Textpattern\Tag\Registry') ->register('smd_article_stats'); } } /** * Public tag: display article statistics. * * @param array $atts Tag attributes * @param string $thing Tag container content * @return string HTML */ function smd_article_stats($atts, $thing = null) { global $thisarticle; assert_article(); extract(lAtts(array( 'wraptag' => '', 'class' => __FUNCTION__, 'break' => '', 'label' => '', 'labeltag' => '', 'item' => '', 'type' => 'word', ), $atts)); $out = array(); // item not specified? Use the array 'keys' from the pref. if (empty($item)) { $fldList = do_list(get_pref('smd_artstat_fields')); $cfs = getCustomFields(); foreach ($fldList as $fld) { $fldInfo = do_list($fld, '->'); $field = $fldInfo[0]; if (strpos($field, 'custom_') !== false) { $cfnum = str_replace('custom_', '', $field); if (array_key_exists($cfnum, $cfs)) { $field = $cfs[$cfnum]; } else { // Bogus CF: skip it continue; } } $item[] = strtolower($field); } } else { $item = do_list($item); } $ret = smd_article_info_count($item, $thisarticle); $out = ($type === 'char') ? $ret['char'] : $ret['word']; return doLabel($label, $labeltag) . doWrap($out, $wraptag, $break, $class); } /** * Admin-side info -- auto-updated via jQuery. * * @param string $event Textpattern event (panel) * @param string $step Textpattern step (action) * @return string HTML */ function smd_article_info($event, $step) { global $app_mode; extract(gpsa(array('view'))); include_once txpath.'/publish/taghandlers.php'; if(!$view || gps('save') || gps('publish')) { $view = 'text'; } if ($view == 'text') { $screen_locs = array( 'none' => '', 'excerpt_below' => 'jq|.excerpt|after', 'author_below' => 'jq|.author|after', 'sort_display_above' => 'jq|#txp-write-sort-group|before', 'sort_display_below' => 'jq|#txp-write-sort-group|after', 'title_above' => 'jq|#main_content .title|prepend', ); // Check hidden pref and sanitize $posn = get_pref('smd_artstat_pos', 'sort_display_above'); $posn = (array_key_exists($posn, $screen_locs)) ? $posn : 'sort_display_above'; $show_words = get_pref('smd_artstat_show_word', '1'); $show_chars = get_pref('smd_artstat_show_char', '1'); $placer = explode('|', $screen_locs[$posn]); doArray($placer, 'escape_js'); // Split and recombine to get rid of spaces. // @todo Error detection if missing entries. $fldList = do_list(get_pref('smd_artstat_fields', 'Body -> #body, Excerpt -> #excerpt')); $fldAnchors = array('0'); // Placeholder since Status isn't a countable field, but we need it later. $db_fields = array('Status'); foreach ($fldList as $fld) { $fldInfo = do_list($fld, '->'); $db_fields[] = $fldInfo[0]; if (isset($fldInfo[1])) { $fldAnchors[] = $fldInfo[1]; } } array_shift($fldAnchors); // Goodbye Status anchor. $js_fields = escape_js(implode(',', $fldAnchors)); $js_array_fields = implode(',', doArray(doArray($fldAnchors, 'escape_js'), 'doQuote')); $id = (empty($GLOBALS['ID']) ? gps('ID') : $GLOBALS['ID']); if (empty($id)) { $rs = $db_fields; } else { $rs = safe_row(implode(',', doArray($db_fields, 'doSlash')), 'textpattern', 'ID=' . doSlash($id)); } $idlink = (get_pref('smd_artstat_id') === '1') ? (($id && in_array($rs['Status'], array(STATUS_LIVE, STATUS_STICKY))) ? href($id, permlinkurl_id($id)) : $id) : ''; $indiv = array( 'word' => array(), 'char' => array(), ); $totals = array( 'word' => 0, 'char' => 0, ); array_shift($db_fields); // Goodbye Status field. $info = smd_article_info_count($db_fields, $rs); foreach ($info as $type => $block) { $counter = 0; foreach ($block as $fld => $qty) { $totals[$type] += $qty; $indiv[$type][] = '' . $qty . ''; $counter++; } } gTxtScript(array('smd_artstat_word_singular', 'smd_artstat_word_plural')); gTxtScript(array('smd_artstat_char_singular', 'smd_artstat_char_plural')); $singstring = get_pref('smd_artstat_singular', '1'); $singles = do_list($singstring); $content = array(); if ($show_words) { $content[] = '' . $totals['word'] . ' ' . (in_array($totals['word'], $singles) ? gTxt('smd_artstat_word_singular') : gTxt('smd_artstat_word_plural')) . ''; if (count($indiv['word']) > 1) { $content[] = ' ( ' . implode(' / ', $indiv['word']) . ' )'; } } if ($show_chars) { $content[] = ($content ? ' | ' : '') . '' . $totals['char'] . ' ' . (in_array($totals['char'], $singles) ? gTxt('smd_artstat_char_singular') : gTxt('smd_artstat_char_plural')) . ''; if (count($indiv['char']) > 1) { $content[] = ' ( ' . implode(' / ', $indiv['char']) . ' )'; } } if ($idlink) { $content[] = ($content ? ' | ' : '') . gTxt('id') . n . $idlink; } $out1 = escape_js( defined('PREF_PLUGIN') ? wrapGroup('smd_artstat', implode(n, $content), 'smd_artstat') : '
'.gTxt('smd_artstat_legend').'

' . implode(n, $content) . '

' ); $out2 = script_js(<< 0) { content = jQuery(flds[idx]).val(); content += (content.length > 0) ? " " : ""; content = content.replace(/(<([^>]+)>)/ig,""); char_count = content.length-1; word_count = content.split(/\s+/).length-1; jQuery(".smd_article_word_stats_"+idx).text(word_count); jQuery(".smd_article_char_stats_"+idx).text(char_count); wds += word_count; chs += char_count; } } jQuery(".smd_article_stats_wc").text(wds); jQuery(".smd_article_stats_cc").text(chs); jQuery(".smd_article_stats_wd").text(((jQuery.inArray(wds, singlist) > -1) ? textpattern.gTxt('smd_artstat_word_singular') : textpattern.gTxt('smd_artstat_word_plural'))); jQuery(".smd_article_stats_cd").text(((jQuery.inArray(chs, singlist) > -1) ? textpattern.gTxt('smd_artstat_char_singular') : textpattern.gTxt('smd_artstat_char_plural'))); }).keyup(); }); EOJS ); if ($placer[0] === 'jq' && $app_mode !== 'async') { echo '' . $out2; } } } /** * Jump to the prefs panel fro the Plugins panel Options link. */ function smd_artstat_options() { $link = '?event=prefs#prefs_group_smd_artstat'; header('Location: ' . $link); } /** * Install prefs if they don't already exist. * * @param string $evt Textpattern event (panel) * @param string $stp Textpattern step (action) */ function smd_artstat_prefs($evt, $stp) { $smd_ai_prefs = smd_article_info_prefs(); foreach ($smd_ai_prefs as $key => $prefobj) { if (get_pref($key) === '') { set_pref($key, doSlash($prefobj['default']), 'smd_artstat', $prefobj['type'], $prefobj['html'], $prefobj['position'], $prefobj['visibility']); } } } /** * Only render these prefs if enough privs exist. * * Restricted message otherwise. * * @param string $key The preference key being displayed * @param string $val The current preference value * @return string HTML */ function smd_artstat_restricted($key, $val) { global $txp_user; static $smd_artstat_privs = array(); if (array_key_exists($txp_user, $smd_artstat_privs)) { $privs = $smd_artstat_privs[$txp_user]; } else { $safe_user = doSlash($txp_user); $privs = safe_field('privs', 'txp_users', "name='$safe_user'"); $smd_artstat_privs[$txp_user] = $privs; } if ($privs === '1') { return fInput('text', $key, $val, '', '', '', INPUT_REGULAR); } else { return gTxt('smd_artstat_set_by_admin'); } } /** * Render the position pref. * * @param string $key The preference key being displayed * @param string $val The current preference value * @return string HTML */ function smd_artstat_pos($key, $val) { $smd_ai_prefs = smd_article_info_prefs(); $obj = $smd_ai_prefs[$key]; return selectInput($key, $obj['content'], $val); } /** * Settings for the plugin. * * @return array Preference set */ function smd_article_info_prefs() { $smd_ai_prefs = array( 'smd_artstat_fields' => array( 'html' => 'smd_artstat_restricted', 'type' => PREF_PLUGIN, 'position' => 10, 'default' => 'Body -> #body, Excerpt -> #excerpt', 'group' => 'smd_artstat_settings', 'visibility' => PREF_GLOBAL, ), 'smd_artstat_pos' => array( 'html' => 'smd_artstat_pos', 'type' => PREF_PLUGIN, 'position' => 20, 'content' => array( 'none' => gTxt('none'), 'title_above' => gTxt('smd_artstat_pos_above_title'), 'excerpt_below' => gTxt('smd_artstat_pos_below_excerpt'), 'author_below' => gTxt('smd_artstat_pos_below_author'), 'sort_display_above' => gTxt('smd_artstat_pos_above_sort_display'), 'sort_display_below' => gTxt('smd_artstat_pos_below_sort_display'), ), 'default' => 'sort_display_above', 'group' => 'smd_artstat_settings', 'visibility' => PREF_PRIVATE, ), 'smd_artstat_singular' => array( 'html' => 'smd_artstat_restricted', 'type' => PREF_PLUGIN, 'position' => 30, 'default' => '1', 'group' => 'smd_artstat_settings', 'visibility' => PREF_GLOBAL, ), 'smd_artstat_show_word' => array( 'html' => 'yesnoradio', 'type' => PREF_PLUGIN, 'position' => 40, 'default' => '1', 'group' => 'smd_artstat_settings', 'visibility' => PREF_PRIVATE, ), 'smd_artstat_show_char' => array( 'html' => 'yesnoradio', 'type' => PREF_PLUGIN, 'position' => 50, 'default' => '0', 'group' => 'smd_artstat_settings', 'visibility' => PREF_PRIVATE, ), 'smd_artstat_id' => array( 'html' => 'yesnoradio', 'type' => PREF_PLUGIN, 'position' => 60, 'default' => '0', 'group' => 'smd_artstat_settings', 'visibility' => PREF_PRIVATE, ), ); return $smd_ai_prefs; } /** * Library function to count words in the given field items. * * @param string|array $item The field(s) to count * @param array $from The structure containing the data * @return array * @todo Filter out common Textile markup somehow? But what about when multi textfilters hit the streets? */ function smd_article_info_count($item, $from) { $words = array(); $chars = array(); $notags = '/(<([^>]+?)>)/'; $item = is_array($item) ? $item : array($item); foreach ($item as $whatnot) { $content = (isset($from[$whatnot])) ? preg_replace($notags, '', trim($from[$whatnot])) . ((strlen($from[$whatnot])==0) ? '' : ' ') : ''; if ($content) { if (!isset($words[$whatnot])) { $words[$whatnot] = 0; } if (!isset($chars[$whatnot])) { $chars[$whatnot] = 0; } $words[$whatnot] += preg_match_all('@\s+@', $content, $m); $chars[$whatnot] += strlen($content) - 1; } } return array('word' => $words, 'char' => $chars); } # --- END PLUGIN CODE --- if (0) { ?>