Value aggregation type (#386)

* Allow a single value to be used inline in the page

* Removed debug line

* Added short form of value syntax - {{$s.f}}

* Refactor inline parsing into a new class and add filtering for page ID

* Allow recovery if the user has used single quotes

* Fixed metadata rendering when using $INFO['id']

* Handle no result better

* Add configuration for whether to show a no records text

* Linter fixes

* Match array style with other entries

* Simplify tokenising with -> separator

* Replace use of GLOBALS array

* Revert to dots, remove block syntax

* Suggestion of future syntax

* Use space-separated parameters

* Explanatory comments

* Increase sort order

* Don't lint for errors required by DokuWiki

* Move syntax description to class documentation

* Draft InlineConfigParser test

* Docstring alignment
This commit is contained in:
Iain Hallam
2021-06-17 13:44:57 +01:00
committed by GitHub
parent 4336569a18
commit 812a20f76a
7 changed files with 456 additions and 0 deletions

View File

@ -0,0 +1,59 @@
<?php
namespace dokuwiki\plugin\struct\test;
use dokuwiki\plugin\struct\meta;
/**
* Tests for parsing the inline aggregation config for the struct plugin
*
* @group plugin_struct
* @group plugins
*
*/
// phpcs:ignore Squiz.Classes.ValidClassName
class InlineConfigParser_struct_test extends StructTest
{
// phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
public function test_simple()
{
// Same initial setup as ConfigParser.test
$inline = '"testtable, another, foo bar"."%pageid%, count" ';
$inline .= '?sort: ^count sort: "%pageid%, ^bam" align: "r,l,center,foo"';
// Add InlineConfigParser-specific tests:
$inline .= '& "%pageid% != start" | "count = 1"';
$configParser = new meta\InlineConfigParser($inline);
$actual_config = $configParser->getConfig();
$expected_config = [
'align' => ['right', 'left', 'center', null],
'cols' => ['%pageid%', 'count'],
'csv' => true,
'dynfilters' => false,
'filter' => [
['%pageid%', '!=', 'start', 'AND'],
['count', '=', '1', 'OR' ],
],
'headers' => [null, null],
'limit' => 0,
'rownumbers' => false,
'schemas' => [
['testtable', '' ],
['another', '' ],
['foo', 'bar'],
],
'sepbyheaders' => false,
'sort' => [
['count', false],
['%pageid%', true ],
['bam', false],
],
'summarize' => false,
'target' => '',
'widths' => [],
];
$this->assertEquals($expected_config, $actual_config);
}
}

View File

@ -3,3 +3,4 @@
$conf['bottomoutput'] = 0;
$conf['topoutput'] = 0;
$conf['disableDeleteSerial'] = 0;
$conf['show_not_found'] = 1;

View File

@ -3,3 +3,4 @@
$meta['bottomoutput'] = ['onoff'];
$meta['topoutput'] = ['onoff'];
$meta['disableDeleteSerial'] = ['onoff'];
$meta['show_not_found'] = ['onoff'];

View File

@ -1,4 +1,6 @@
<?php
$lang['bottomoutput'] = 'Display data at the bottom of the page';
$lang['topoutput'] = 'Display data at the top of the page';
$lang['disableDeleteSerial'] = 'Disable delete button for serial data';
$lang['show_not_found'] = 'Show the default text when no results are returned for struct value syntax';

162
meta/AggregationValue.php Normal file
View File

@ -0,0 +1,162 @@
<?php
namespace dokuwiki\plugin\struct\meta;
/**
* Class AggregationValue
*
* @package dokuwiki\plugin\struct\meta
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
* @author Iain Hallam <iain@nineworlds.net>
*/
class AggregationValue
{
/**
* @var string the page id of the page this is rendered to
*/
protected $id;
/**
* @var string the Type of renderer used
*/
protected $mode;
/**
* @var Doku_Renderer the DokuWiki renderer used to create the output
*/
protected $renderer;
/**
* @var SearchConfig the configured search - gives access to columns etc.
*/
protected $searchConfig;
/**
* @var Column the column to be displayed
*/
protected $column;
/**
* @var Value[][] the search result
*/
protected $result;
/**
* @var int number of all results
*/
protected $resultCount;
/**
* @todo we might be able to get rid of this helper and move this to SearchConfig
* @var helper_plugin_struct_config
*/
protected $helper;
/**
* Initialize the Aggregation renderer and executes the search
*
* You need to call @see render() on the resulting object.
*
* @param string $id
* @param string $mode
* @param Doku_Renderer $renderer
* @param SearchConfig $searchConfig
*/
public function __construct($id, $mode, \Doku_Renderer $renderer, SearchConfig $searchConfig)
{
// Parameters
$this->id = $id;
$this->mode = $mode;
$this->renderer = $renderer;
$this->searchConfig = $searchConfig;
// Search info
$this->data = $this->searchConfig->getConf();
$columns = $this->searchConfig->getColumns();
$this->column = $columns[0];
// limit to first result
$this->searchConfig->setLimit(1);
$this->searchConfig->setOffset(0);
// Run the search
$result = $this->searchConfig->execute();
$this->resultCount = $this->searchConfig->getCount();
// Change from two-dimensional array with one entry to one-dimensional array
$this->result = $result[0];
// Load helper
$this->helper = plugin_load('helper', 'struct_config');
}
/**
* Create the output on the renderer
*
* @param int $show_not_found Whether to display the default text for no records
*/
public function render($show_not_found = 0)
{
$this->startScope();
// Check that we actually got a result
if ($this->resultCount) {
$this->renderValue($this->result);
} else {
if ($show_not_found) {
$this->renderer->cdata($this->helper->getLang('none'));
}
}
$this->finishScope();
return;
}
/**
* Adds additional info to document and renderer in XHTML mode
*
* @see finishScope()
*/
protected function startScope()
{
// wrapping span
if ($this->mode != 'xhtml') {
return;
}
$this->renderer->doc .= "<span class=\"structaggregation valueaggregation\">";
}
/**
* Closes anything opened in startScope()
*
* @see startScope()
*/
protected function finishScope()
{
// wrapping span
if ($this->mode != 'xhtml') {
return;
}
$this->renderer->doc .= '</span>';
}
/**
* @param $resultrow
*/
protected function renderValue($resultrow)
{
// @var Value $value
foreach ($resultrow as $column => $value) {
if ($value->isEmpty()) {
continue;
}
if ($this->mode == 'xhtml') {
$type = 'struct_' . strtolower($value->getColumn()->getType()->getClass());
$this->renderer->doc .= '<span class="' . $type . '">';
}
$value->render($this->renderer, $this->mode);
if ($this->mode == 'xhtml') {
$this->renderer->doc .= '</span>';
}
}
}
}

103
meta/InlineConfigParser.php Normal file
View File

@ -0,0 +1,103 @@
<?php
namespace dokuwiki\plugin\struct\meta;
/**
* Class InlineConfigParser
*
* Wrapper to convert inline syntax to full before instantiating ConfigParser
*
* {{$schema.field}}
* {{$pageid.schema.field}}
* {{$... ? filter: ... and: ... or: ...}} or {{$... ? & ... | ...}}
* TODO: {{$... ? sum}} or {{$... ? +}}
* TODO: {{$... ? default: ...}} or {{$... ? ! ...}}
* Colons following key words must have no space preceding them.
* If no page ID or filter is supplied, filter: "%pageid% = $ID$" is added.
* Any component can be placed in double quotes (needed to allow space, dot or question mark in components).
*
* @package dokuwiki\plugin\struct\meta
*/
class InlineConfigParser extends ConfigParser
{
/**
* Parser constructor.
*
* parses the given inline configuration
*
* @param string $inline
*/
public function __construct($inline)
{
// Start to build the main config array
$lines = array(); // Config lines to pass to full parser
// Extract components
$parts = explode('?', $inline, 2);
$n_parts = count($parts);
$components = str_getcsv(trim($parts[0]), '.');
$n_components = count($components);
// Extract parameters if given
if ($n_parts == 2) {
$filtering = false; // Whether to filter result to current page
$parameters = str_getcsv(trim($parts[1]), ' ');
$n_parameters = count($parameters);
// Process parameters and add to config lines
for ($i = 0; $i < $n_parameters; $i++) {
$p = trim($parameters[$i]);
switch ($p) {
// Empty (due to extra spaces)
case '':
// Move straight to next parameter
continue 2;
break;
// Pass full text ending in : straight to config
case $p[-1] == ':' ? $p : '':
if (in_array($p, ['filter', 'where', 'filterand', 'and', 'filteror','or'])) {
$filtering = true;
}
$lines[] = $p . ' ' . trim($parameters[$i + 1]);
$i++;
break;
// Short alias for filterand
case '&':
$filtering = true;
$lines[] = 'filterand: ' . trim($parameters[$i + 1]);
$i++;
break;
// Short alias for filteror
case '|':
$filtering = true;
$lines[] = 'filteror: ' . trim($parameters[$i + 1]);
$i++;
break;
default:
// Move straight to next parameter
continue 2;
break;
}
}
}
// Check whether a page was specified
if (count($components) == 3) {
// At least page, schema and field supplied
$lines[] = 'schema: ' . trim($components[1]);
$lines[] = 'field: ' . trim($components[2]);
$lines[] = 'filter: %pageid% = ' . trim($components[0]);
} elseif (count($components) == 2) {
// At least schema and field supplied
$lines[] = 'schema: ' . trim($components[0]);
$lines[] = 'field: ' . trim($components[1]);
if (! $filtering) {
$lines[] = 'filter: %pageid% = $ID$';
}
}
// Call original ConfigParser's constructor
parent::__construct($lines);
}
}

128
syntax/value.php Normal file
View File

@ -0,0 +1,128 @@
<?php
/**
* DokuWiki Plugin struct (Syntax Component)
*
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
* @author Iain Hallam <iain@nineworlds.net>
*/
// phpcs:disable PSR1.Files.SideEffects
use dokuwiki\plugin\struct\meta\AggregationValue;
use dokuwiki\plugin\struct\meta\ConfigParser;
use dokuwiki\plugin\struct\meta\InlineConfigParser;
use dokuwiki\plugin\struct\meta\SearchConfig;
use dokuwiki\plugin\struct\meta\StructException;
// must be run within Dokuwiki
if (!defined('DOKU_INC')) {
die();
}
// phpcs:ignore PSR1.Classes.ClassDeclaration, Squiz.Classes.ValidClassName
class syntax_plugin_struct_value extends DokuWiki_Syntax_Plugin
{
/**
* @return string Syntax mode type
*/
public function getType()
{
return 'substition';
}
/**
* @return int Sort order - Low numbers go before high numbers
*/
public function getSort()
{
/* 315 to place above Doku_Parser_Mode_media, which would
* otherwise take precedence. See
* https://www.dokuwiki.org/devel:parser:getsort_list
*/
return 315;
}
/**
* Connect lookup pattern to lexer.
*
* @param string $mode Parser mode
*/
public function connectTo($mode)
{
// {{$...}}
$this->Lexer->addSpecialPattern('\{\{\$[^}]+\}\}', $mode, 'plugin_struct_value');
}
/**
* Handle matches of the struct syntax
*
* @param string $match The match of the syntax
* @param int $state The state of the handler
* @param int $pos The position in the document
* @param Doku_Handler $handler The handler
* @return array Data for the renderer
*/
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $conf;
try {
// strip {{$ and }} markers
$inline = substr($match, 3, -2);
// Parse inline syntax
$parser = new InlineConfigParser($inline);
$config = $parser->getConfig();
return $config;
} catch (StructException $e) {
msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
if ($conf['allowdebug']) {
msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1);
}
return null;
}
}
/**
* Render xhtml output or metadata
*
* @param string $mode Renderer mode (supported modes: xhtml)
* @param Doku_Renderer $renderer The renderer
* @param array $data The data from the handler() function
* @return bool If rendering was successful.
*/
public function render($mode, Doku_Renderer $renderer, $data)
{
if (!$data) {
return false;
}
global $INFO;
global $conf;
// Get configuration
$show_not_found = $this->getConf('show_not_found');
try {
/** @var SearchConfig $search */
$search = new SearchConfig($data);
/** @var AggregationValue $value */
$value = new AggregationValue($INFO['id'], $mode, $renderer, $search);
$value->render($show_not_found);
if ($mode == 'metadata') {
/** @var Doku_Renderer_metadata $renderer */
$renderer->meta['plugin']['struct']['hasaggregation'] = $search->getCacheFlag();
}
} catch (StructException $e) {
msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
if ($conf['allowdebug']) {
msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1);
}
}
return true;
}
}