diff --git a/_test/ConfigParser.test.php b/_test/ConfigParser.test.php
index 77b2a46403..8cc68a567a 100644
--- a/_test/ConfigParser.test.php
+++ b/_test/ConfigParser.test.php
@@ -78,6 +78,7 @@ public function test_simple() {
)
),
'csv' => true,
+ 'target' => ''
);
$this->assertEquals($expected_config, $actual_config);
diff --git a/_test/Type_Page.test.php b/_test/Type_Page.test.php
index 4cf7157b79..22274e2b36 100644
--- a/_test/Type_Page.test.php
+++ b/_test/Type_Page.test.php
@@ -108,6 +108,16 @@ public function test_search() {
$result[0][3]->getValue()
);
+ // if there is no title in the database display the pageid
+ $this->assertEquals(
+ array(
+ 'DokuWiki Overview',
+ 'DokuWiki Foobar Syntax',
+ 'wiki:welcome'
+ ),
+ $result[0][3]->getDisplayValue()
+ );
+
// search single with title
$single = clone $search;
$single->addFilter('singletitle', 'Overview', '*~', 'AND');
diff --git a/lang/en/lang.php b/lang/en/lang.php
index 40700820ac..2b0d89d5d3 100644
--- a/lang/en/lang.php
+++ b/lang/en/lang.php
@@ -80,6 +80,8 @@
$lang['Exception No data saved'] = 'No data saved';
$lang['Exception no sqlite'] = 'The struct plugin requires the sqlite plugin. Please install and enable it.';
+$lang['Warning: no filters for cloud'] = 'Filters are not supported for struct clouds.';
+
$lang['sort'] = 'Sort by this column';
$lang['next'] = 'Next page';
$lang['prev'] = 'Previous page';
diff --git a/meta/AggregationCloud.php b/meta/AggregationCloud.php
new file mode 100644
index 0000000000..8919b80700
--- /dev/null
+++ b/meta/AggregationCloud.php
@@ -0,0 +1,193 @@
+id = $id;
+ $this->mode = $mode;
+ $this->renderer = $renderer;
+ $this->searchConfig = $searchConfig;
+ $this->data = $searchConfig->getConf();
+ $this->columns = $searchConfig->getColumns();
+ $this->result = $this->searchConfig->execute();
+ $this->resultCount = $this->searchConfig->getCount();
+
+ $this->max = $this->result[0]['count'];
+ $this->min = end($this->result)['count'];
+ }
+
+ /**
+ * Create the cloud on the renderer
+ */
+ public function render() {
+
+ $this->sortResults();
+
+ $this->startScope();
+ $this->startList();
+ foreach ($this->result as $result) {
+ $this->renderTag($result);
+ }
+ $this->finishList();
+ $this->finishScope();
+ return;
+ }
+
+ /**
+ * Adds additional info to document and renderer in XHTML mode
+ *
+ * @see finishScope()
+ */
+ protected function startScope() {
+ // wrapping div
+ if($this->mode != 'xhtml') return;
+ $this->renderer->doc .= "
";
+ }
+
+ /**
+ * Closes the table and anything opened in startScope()
+ *
+ * @see startScope()
+ */
+ protected function finishScope() {
+ // wrapping div
+ if($this->mode != 'xhtml') return;
+ $this->renderer->doc .= '
';
+ }
+
+ /**
+ * Render a tag of the cloud
+ *
+ * @param ['tag' => Value, 'count' => int] $result
+ */
+ protected function renderTag($result) {
+ /**
+ * @var Value $value
+ */
+ $value = $result['tag'];
+ $count = $result['count'];
+ if ($value->isEmpty()) {
+ return;
+ }
+
+ $type = strtolower($value->getColumn()->getType()->getClass());
+ $weight = $this->getWeight($count, $this->min, $this->max);
+
+ if (!empty($this->data['target'])) {
+ $target = $this->data['target'];
+ } else {
+ global $INFO;
+ $target = $INFO['id'];
+ }
+
+ $tagValue = $value->getDisplayValue();
+ if (is_array($tagValue)) {
+ $tagValue = $tagValue[0];
+ }
+ $key = $value->getColumn()->getFullQualifiedLabel() . '*~';
+ $filter = SearchConfigParameters::$PARAM_FILTER . "[$key]=" . urlencode($tagValue);
+
+ $this->renderer->listitem_open(1);
+ $this->renderer->listcontent_open();
+
+ if($this->mode == 'xhtml') {
+ $this->renderer->doc .= "";
+ }
+
+ $value->renderAsTagCloudLink($this->renderer, $this->mode, $target, $filter, $weight);
+
+ if($this->mode == 'xhtml') {
+ $this->renderer->doc .= '
';
+ }
+
+ $this->renderer->listcontent_close();
+ $this->renderer->listitem_close();
+ }
+
+ /**
+ * This interpolates the weight between 70 and 150 based on $min, $max and $current
+ *
+ * @param int $current
+ * @param int $min
+ * @param int $max
+ * @return int
+ */
+ protected function getWeight($current, $min, $max) {
+ if ($min == $max) {
+ return 100;
+ }
+ return round(($current - $min)/($max - $min) * 80 + 70);
+ }
+
+ /**
+ * Sort the list of results
+ */
+ protected function sortResults() {
+ usort($this->result, function ($a, $b) {
+ $asort = $a['tag']->getColumn()->getType()->getSortString($a['tag']);
+ $bsort = $b['tag']->getColumn()->getType()->getSortString($b['tag']);
+ if ($asort < $bsort) {
+ return -1;
+ }
+ if ($asort > $bsort) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ protected function startList() {
+ $this->renderer->listu_open();
+ }
+
+ protected function finishList() {
+ $this->renderer->listu_close();
+ }
+}
diff --git a/meta/ConfigParser.php b/meta/ConfigParser.php
index 596536c565..bddb99a3f7 100644
--- a/meta/ConfigParser.php
+++ b/meta/ConfigParser.php
@@ -29,6 +29,7 @@ public function __construct($lines) {
'summarize' => false,
'rownumbers' => false,
'sepbyheaders' => false,
+ 'target' => '',
'headers' => array(),
'widths' => array(),
'filter' => array(),
@@ -108,6 +109,10 @@ public function __construct($lines) {
case 'csv':
$this->config['csv'] = (bool) $val;
break;
+ case 'target':
+ case 'page':
+ $this->config['target'] = cleanID($val);
+ break;
default:
throw new StructException("unknown option '%s'", hsc($key));
}
diff --git a/meta/SearchCloud.php b/meta/SearchCloud.php
new file mode 100644
index 0000000000..1728519a63
--- /dev/null
+++ b/meta/SearchCloud.php
@@ -0,0 +1,108 @@
+columns) throw new StructException('nocolname');
+
+ $QB = new QueryBuilder();
+ reset($this->schemas);
+ $schema = current($this->schemas);
+ $datatable = 'data_' . $schema->getTable();
+ if(!$schema->isLookup()) {
+ $QB->addTable('schema_assignments');
+ $QB->filters()->whereAnd("$datatable.pid = schema_assignments.pid");
+ $QB->filters()->whereAnd("schema_assignments.tbl = '{$schema->getTable()}'");
+ $QB->filters()->whereAnd("schema_assignments.assigned = 1");
+ $QB->filters()->whereAnd("GETACCESSLEVEL($datatable.pid) > 0");
+ $QB->filters()->whereAnd("PAGEEXISTS($datatable.pid) = 1");
+ }
+ $QB->addTable($datatable);
+ $QB->filters()->whereAnd("$datatable.latest = 1");
+
+ $col = $this->columns[0];
+ if($col->isMulti()) {
+ $multitable = "multi_{$col->getTable()}";
+ $MN = $QB->generateTableAlias('M');
+
+ $QB->addLeftJoin(
+ $datatable,
+ $multitable,
+ $MN,
+ "$datatable.pid = $MN.pid AND
+ $datatable.rev = $MN.rev AND
+ $MN.colref = {$col->getColref()}"
+ );
+
+ $col->getType()->select($QB, $MN, 'value', 'tag');
+ $colname = $MN . '.value';
+ } else {
+ $col->getType()->select($QB, $datatable, $col->getColName(), 'tag');
+ $colname = $datatable . '.' . $col->getColName();
+ }
+ $QB->addSelectStatement("COUNT($colname)", 'count');
+ $QB->addGroupByStatement('tag');
+ $QB->addOrderBy('count DESC');
+
+ list($sql, $opts) = $QB->getSQL();
+ return [$sql . $this->limit, $opts];
+ }
+
+ /**
+ * We do not have pagination in clouds, so we can work with a limit within SQL
+ *
+ * @param int $limit
+ */
+ public function setLimit($limit) {
+ $this->limit = " LIMIT $limit";
+ }
+
+ /**
+ * Execute this search and return the result
+ *
+ * The result is a two dimensional array of Value()s.
+ *
+ * @return Value[][]
+ */
+ public function execute() {
+ list($sql, $opts) = $this->getSQL();
+
+ /** @var \PDOStatement $res */
+ $res = $this->sqlite->query($sql, $opts);
+ if($res === false) throw new StructException("SQL execution failed for\n\n$sql");
+
+ $result = [];
+ $rows = $this->sqlite->res2arr($res);
+
+ foreach ($rows as $row) {
+ if (!empty($this->config['min']) && $this->config['min'] > $row['count']) {
+ break;
+ }
+
+ $row['tag'] = new Value($this->columns[0], $row['tag']);
+ $result[] = $row;
+ }
+
+ $this->sqlite->res_close($res);
+ $this->count = count($result);
+ return $result;
+ }
+}
diff --git a/meta/Value.php b/meta/Value.php
index 59157e4892..3c9e10cd07 100644
--- a/meta/Value.php
+++ b/meta/Value.php
@@ -154,6 +154,19 @@ public function render(\Doku_Renderer $R, $mode) {
return true;
}
+ /**
+ * Render this value as a tag-link in a struct cloud
+ *
+ * @param \Doku_Renderer $R
+ * @param string $mode
+ * @param string $page
+ * @param string $filterQuery
+ * @param int $weight
+ */
+ public function renderAsTagCloudLink(\Doku_Renderer $R, $mode, $page, $filterQuery, $weight) {
+ $this->column->getType()->renderTagCloudLink($this->value, $R, $mode, $page, $filterQuery, $weight);
+ }
+
/**
* Return the value editor for this value field
*
diff --git a/style.less b/style.less
index 2c69ccdf8c..9e876c2413 100644
--- a/style.less
+++ b/style.less
@@ -305,6 +305,29 @@ form.struct_newschema {
background-position-x: 2px;
}
}
+
+.dokuwiki .structcloud {
+ li {
+ float: left;
+ list-style: none;
+ margin: 0 1em 0 0;
+ padding: 0;
+
+ .struct_color, .struct_media {
+ a {
+ display: block;
+ height: 100%;
+ }
+ }
+
+ .struct_media a {
+ background-size: cover;
+ background-position: center;
+ box-shadow: 1px 1px 5px 1px rgba(0,0,0,0.5)
+ }
+ }
+}
+
/**
* Lookup Aggregation Editor
*/
diff --git a/syntax/cloud.php b/syntax/cloud.php
new file mode 100644
index 0000000000..c622868ef9
--- /dev/null
+++ b/syntax/cloud.php
@@ -0,0 +1,106 @@
+
+ */
+
+// must be run within Dokuwiki
+if (!defined('DOKU_INC')) die();
+
+use dokuwiki\plugin\struct\meta\ConfigParser;
+use dokuwiki\plugin\struct\meta\SearchCloud;
+use dokuwiki\plugin\struct\meta\StructException;
+use dokuwiki\plugin\struct\meta\AggregationCloud;
+
+class syntax_plugin_struct_cloud extends DokuWiki_Syntax_Plugin {
+
+ /**
+ * @return string Syntax mode type
+ */
+ public function getType() {
+ return 'substition';
+ }
+ /**
+ * @return string Paragraph type
+ */
+ public function getPType() {
+ return 'block';
+ }
+ /**
+ * @return int Sort order - Low numbers go before high numbers
+ */
+ public function getSort() {
+ return 151;
+ }
+
+ /**
+ * Connect lookup pattern to lexer.
+ *
+ * @param string $mode Parser mode
+ */
+ public function connectTo($mode) {
+ $this->Lexer->addSpecialPattern('----+ *struct cloud *-+\n.*?\n----+',$mode,'plugin_struct_cloud');
+ }
+
+ /**
+ * 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;
+ $lines = explode("\n", $match);
+ array_shift($lines);
+ array_pop($lines);
+
+ try {
+ $parser = new ConfigParser($lines);
+ $config = $parser->getConfig();
+ return $config;
+ } catch(StructException $e) {
+ msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
+ if($conf['allowdebug']) msg('' . hsc($e->getTraceAsString()) . '
', -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($mode != 'xhtml') return false;
+ if(!$data) return false;
+ if (!empty($data['filter'])) {
+ msg($this->getLang('Warning: no filters for cloud'), -1);
+ }
+ global $INFO, $conf;
+ try {
+ $search = new SearchCloud($data);
+ $cloud = new AggregationCloud($INFO['id'], $mode, $renderer, $search);
+ $cloud->render();
+ 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('' . hsc($e->getTraceAsString()) . '
', -1);
+ }
+
+ return true;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/types/AbstractBaseType.php b/types/AbstractBaseType.php
index fee1d14548..7a295c72d1 100644
--- a/types/AbstractBaseType.php
+++ b/types/AbstractBaseType.php
@@ -6,6 +6,7 @@
use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
use dokuwiki\plugin\struct\meta\StructException;
use dokuwiki\plugin\struct\meta\ValidationException;
+use dokuwiki\plugin\struct\meta\Value;
/**
* Class AbstractBaseType
@@ -357,6 +358,20 @@ public function renderMultiValue($values, \Doku_Renderer $R, $mode) {
return true;
}
+ /**
+ * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary.
+ *
+ * @param string|int $value the value stored in the database
+ * @param \Doku_Renderer $R the renderer currently used to render the data
+ * @param string $mode The mode the output is rendered in (eg. XHTML)
+ * @param string $page the target to which should be linked
+ * @param string $filter the filter to apply to the aggregations on $page
+ * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container
+ */
+ public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) {
+ $R->internallink("$page?$filter", $this->displayValue($value));
+ }
+
/**
* This function is used to modify an aggregation query to add a filter
* for the given column matching the given value. A type should add at
@@ -429,6 +444,27 @@ public function sort(QueryBuilder $QB, $tablealias, $colname, $order) {
$QB->addOrderBy("$tablealias.$colname $order");
}
+ /**
+ * Get the string by which to sort values of this type
+ *
+ * This implementation is designed to work both as registered function in sqlite
+ * and to provide a string to be used in sorting values of this type in PHP.
+ *
+ * @param string|Value $string The string by which the types would usually be sorted
+ *
+ * @return string
+ */
+ public function getSortString($value) {
+ if (is_string($value)) {
+ return $value;
+ }
+ $display = $value->getDisplayValue();
+ if (is_array($display)) {
+ return blank($display[0]) ? "" : $display[0];
+ }
+ return $display;
+ }
+
/**
* This allows types to apply a transformation to the value read by select()
*
diff --git a/types/Color.php b/types/Color.php
index 10dc08dcab..f183ac05df 100644
--- a/types/Color.php
+++ b/types/Color.php
@@ -68,4 +68,60 @@ public function valueEditor($name, $rawvalue) {
return "$html";
}
+ /**
+ * @inheritDoc
+ */
+ public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) {
+ $color = $this->displayValue($value);
+ if ($mode == 'xhtml') {
+ $url = wl($page, $filter);
+ $style = "background-color:$color;";
+ $R->doc .= "$color";
+ return;
+ }
+ $R->internallink("$page?$filter", $color);
+ }
+
+
+ /**
+ * Sort by the hue of a color, not by its hex-representation
+ */
+ public function getSortString($value) {
+ $hue = $this->getHue(parent::getSortString($value));
+ return $hue;
+ }
+
+ /**
+ * Calculate the hue of a color to use it for sorting so we can sort similar colors together.
+ *
+ * @param string $color the color as #RRGGBB
+ * @return float|int
+ */
+ protected function getHue($color) {
+ if (!preg_match('/^#[0-9A-F]{6}$/i', $color)) {
+ return 0;
+ }
+
+ $red = hexdec(substr($color, 1, 2));
+ $green = hexdec(substr($color, 3, 2));
+ $blue = hexdec(substr($color, 5, 2));
+
+ $min = min([$red, $green, $blue]);
+ $max = max([$red, $green, $blue]);
+
+ if ($max == $red) {
+ $hue = ($green-$blue)/($max-$min);
+ }
+ if ($max == $green) {
+ $hue = 2 + ($blue-$red)/($max-$min);
+ }
+ if ($max == $blue) {
+ $hue = 4 + ($red-$green)/($max-$min);
+ }
+ $hue = $hue * 60;
+ if ($hue < 0) {
+ $hue += 360;
+ }
+ return $hue;
+ }
}
diff --git a/types/Media.php b/types/Media.php
index fa6c02550d..4ddda478fd 100644
--- a/types/Media.php
+++ b/types/Media.php
@@ -111,4 +111,25 @@ public function valueEditor($name, $rawvalue) {
$html .= "";
return $html;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) {
+ $media = $this->displayValue($value);
+ if ($mode == 'xhtml' && $this->getConfig()['mime'] == 'image/') {
+ $url = wl($page, $filter);
+ $image = ml($media, ['h' => $weight, 'w' => $weight]);
+ $media_escaped = hsc($media);
+ $R->doc .= "";
+ return;
+ }
+ $R->internallink("$page?$filter", $media);
+ }
+
+
}
diff --git a/types/Page.php b/types/Page.php
index f1b014fc67..f190d4fa8b 100644
--- a/types/Page.php
+++ b/types/Page.php
@@ -176,7 +176,10 @@ public function rawValue($value) {
*/
public function displayValue($value) {
if($this->config['usetitles']) {
- list(, $value) = json_decode($value);
+ list($pageid, $value) = json_decode($value);
+ if (blank($value)) {
+ $value = $pageid;
+ }
}
return $value;
}