Skip to content

Commit 78a2651

Browse files
committed
Add experimental tree builder classes
These classes provide mechanisms to build a traversable tree of pages and links. Either from the existing namespace structure, or from a control page containing (possibly a nested) set of links. The nodes returned by the tree are deliberately sparse. No ACL checking is taking place. Developers can enrich (or omit) nodes and influence recursion decisions via callbacks. The tree can optionally be sorted by comparators provided in the TreeSort class or a custom callback. The API provided by these classes is not considered stable yet and may change over time. Plugin authors are encouraged to use them and provide feedback.
1 parent 19f3aa3 commit 78a2651

File tree

14 files changed

+1435
-0
lines changed

14 files changed

+1435
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace dokuwiki\test\Treebuilder;
4+
5+
use dokuwiki\TreeBuilder\ControlPageBuilder;
6+
use DokuWikiTest;
7+
8+
class ControlPageBuilderTest extends DokuWikiTest
9+
{
10+
public static function setUpBeforeClass(): void
11+
{
12+
parent::setUpBeforeClass();
13+
saveWikiText('simple', file_get_contents(__DIR__ . '/cp/simple.txt'), 'test');
14+
saveWikiText('foo:complex', file_get_contents(__DIR__ . '/cp/complex.txt'), 'test');
15+
}
16+
17+
public function testSimpleParsing()
18+
{
19+
$control = new ControlPageBuilder('simple');
20+
$control->generate();
21+
22+
$expected = [
23+
'+briefs:start',
24+
'+qhsr:start',
25+
'++qhsr:q',
26+
'++qhsr:cert',
27+
'++qhsr:hse:start',
28+
'++qhsr:engsystems',
29+
'++qhsr:performance',
30+
'++qhsr:competence',
31+
'++qhsr:ashford',
32+
'++qhsr:training',
33+
'+tech:start',
34+
'+https://homepage.company.com'
35+
];
36+
37+
$result = explode("\n", (string)$control);
38+
sort($expected);
39+
sort($result);
40+
41+
$this->assertEquals($expected, $result);
42+
43+
// Additional structure tests
44+
$top = $control->getTop();
45+
$this->assertEquals(4, count($top->getChildren()));
46+
$this->assertEquals(1, count($top->getChildren()[0]->getParents()));
47+
$this->assertEquals(4, count($top->getChildren()[1]->getSiblings()));
48+
$this->assertEquals(8, count($top->getChildren()[1]->getChildren()));
49+
50+
$this->assertEquals(12, count($control->getAll()));
51+
$this->assertEquals(11, count($control->getLeaves()));
52+
$this->assertEquals(1, count($control->getBranches()));
53+
}
54+
55+
/**
56+
* Parse the complex example with different flags
57+
*
58+
* @return array[]
59+
* @see testComplexParsing
60+
*/
61+
public function complexProvider()
62+
{
63+
return [
64+
'No flags' => [
65+
'flags' => 0,
66+
'expected' => [
67+
'+content',
68+
'+foo:this',
69+
'+foo:bar',
70+
'+foo:another_link',
71+
'+https://www.google.com',
72+
'+relativeup',
73+
'+foo2:this',
74+
'++foo2:deeper:item',
75+
'+++foo2:deeper:evendeeper:item',
76+
'+foo:blarg:down',
77+
'+toplevel',
78+
'+foo:link',
79+
]
80+
],
81+
'FLAG_NOEXTERNAL' => [
82+
'flags' => ControlPageBuilder::FLAG_NOEXTERNAL,
83+
'expected' => [
84+
'+content',
85+
'+foo:this',
86+
'+foo:bar',
87+
'+foo:another_link',
88+
'+relativeup',
89+
'+foo2:this',
90+
'++foo2:deeper:item',
91+
'+++foo2:deeper:evendeeper:item',
92+
'+foo:blarg:down',
93+
'+toplevel',
94+
'+foo:link',
95+
]
96+
],
97+
'FLAG_NOINTERNAL' => [
98+
'flags' => ControlPageBuilder::FLAG_NOINTERNAL,
99+
'expected' => [
100+
'+https://www.google.com',
101+
]
102+
],
103+
];
104+
}
105+
106+
/**
107+
* @dataProvider complexProvider
108+
* @param int $flags
109+
* @param array $expected
110+
* @return void
111+
*/
112+
public function testComplexParsing(int $flags, array $expected)
113+
{
114+
$control = new ControlPageBuilder('foo:complex');
115+
$control->addFlag($flags);
116+
$control->generate();
117+
118+
$result = explode("\n", (string)$control);
119+
sort($expected);
120+
sort($result);
121+
122+
$this->assertEquals($expected, $result);
123+
}
124+
125+
public function testNonExisting()
126+
{
127+
$this->expectException(\RuntimeException::class);
128+
$control = new ControlPageBuilder('does:not:exist');
129+
$control->generate();
130+
$foo = $control->getAll();
131+
}
132+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<?php
2+
3+
namespace dokuwiki\test\Treebuilder;
4+
5+
use dokuwiki\TreeBuilder\PageTreeBuilder;
6+
use DokuWikiTest;
7+
8+
class PageTreeBuilderTest extends DokuWikiTest
9+
{
10+
protected $originalDataDir;
11+
12+
public static function setUpBeforeClass(): void
13+
{
14+
parent::setUpBeforeClass();
15+
16+
// Create a test page hierarchy
17+
saveWikiText('namespace:start', 'This is the start page', 'test');
18+
saveWikiText('namespace:page1', 'This is page 1', 'test');
19+
saveWikiText('namespace:page2', 'This is page 2', 'test');
20+
saveWikiText('namespace:subns:start', 'This is the subns start page', 'test');
21+
saveWikiText('namespace:subns:page3', 'This is page 3 in subns', 'test');
22+
saveWikiText('namespace:subns:deeper:start', 'This is the deeper start page', 'test');
23+
saveWikiText('namespace:subns:deeper:page4', 'This is page 4 in deeper', 'test');
24+
}
25+
26+
public function setUp(): void
27+
{
28+
parent::setUp();
29+
global $conf;
30+
$this->originalDataDir = $conf['datadir'];
31+
}
32+
33+
public function tearDown(): void
34+
{
35+
global $conf;
36+
$conf['datadir'] = $this->originalDataDir;
37+
parent::tearDown();
38+
}
39+
40+
public function treeConfigProvider()
41+
{
42+
return [
43+
'Default configuration' => [
44+
'namespace' => 'namespace',
45+
'depth' => -1,
46+
'flags' => 0,
47+
'expected' => [
48+
'+namespace:start',
49+
'+namespace:page1',
50+
'+namespace:page2',
51+
'+namespace:subns',
52+
'++namespace:subns:start',
53+
'++namespace:subns:page3',
54+
'++namespace:subns:deeper',
55+
'+++namespace:subns:deeper:start',
56+
'+++namespace:subns:deeper:page4'
57+
]
58+
],
59+
'Depth limit 1' => [
60+
'namespace' => 'namespace',
61+
'depth' => 1,
62+
'flags' => 0,
63+
'expected' => [
64+
'+namespace:start',
65+
'+namespace:page1',
66+
'+namespace:page2',
67+
'+namespace:subns',
68+
'++namespace:subns:start',
69+
'++namespace:subns:page3',
70+
'++namespace:subns:deeper'
71+
]
72+
],
73+
'Depth limit 1 with NS_AS_STARTPAGE' => [
74+
'namespace' => 'namespace',
75+
'depth' => 1,
76+
'flags' => PageTreeBuilder::FLAG_NS_AS_STARTPAGE,
77+
'expected' => [
78+
'+namespace:page1',
79+
'+namespace:page2',
80+
'+namespace:subns:start',
81+
'++namespace:subns:page3',
82+
'++namespace:subns:deeper:start'
83+
]
84+
],
85+
'FLAG_NO_NS' => [
86+
'namespace' => 'namespace',
87+
'depth' => -1,
88+
'flags' => PageTreeBuilder::FLAG_NO_NS,
89+
'expected' => [
90+
'+namespace:start',
91+
'+namespace:page1',
92+
'+namespace:page2'
93+
]
94+
],
95+
'FLAG_NO_PAGES' => [
96+
'namespace' => 'namespace',
97+
'depth' => -1,
98+
'flags' => PageTreeBuilder::FLAG_NO_PAGES,
99+
'expected' => [
100+
'+namespace:subns',
101+
'++namespace:subns:deeper'
102+
]
103+
],
104+
'FLAG_NS_AS_STARTPAGE' => [
105+
'namespace' => 'namespace',
106+
'depth' => -1,
107+
'flags' => PageTreeBuilder::FLAG_NS_AS_STARTPAGE,
108+
'expected' => [
109+
'+namespace:page1',
110+
'+namespace:page2',
111+
'+namespace:subns:start',
112+
'++namespace:subns:page3',
113+
'++namespace:subns:deeper:start',
114+
'+++namespace:subns:deeper:page4'
115+
]
116+
],
117+
'Combined FLAG_NO_NS and FLAG_NS_AS_STARTPAGE' => [
118+
'namespace' => 'namespace',
119+
'depth' => -1,
120+
'flags' => PageTreeBuilder::FLAG_NO_NS | PageTreeBuilder::FLAG_NS_AS_STARTPAGE,
121+
'expected' => [
122+
'+namespace:page1',
123+
'+namespace:page2'
124+
]
125+
],
126+
'FLAG_SELF_TOP' => [
127+
'namespace' => 'namespace',
128+
'depth' => -1,
129+
'flags' => PageTreeBuilder::FLAG_SELF_TOP,
130+
'expected' => [
131+
'+namespace',
132+
'++namespace:start',
133+
'++namespace:page1',
134+
'++namespace:page2',
135+
'++namespace:subns',
136+
'+++namespace:subns:start',
137+
'+++namespace:subns:page3',
138+
'+++namespace:subns:deeper',
139+
'++++namespace:subns:deeper:start',
140+
'++++namespace:subns:deeper:page4'
141+
]
142+
],
143+
];
144+
}
145+
146+
147+
/**
148+
* @dataProvider treeConfigProvider
149+
*/
150+
public function testPageTreeConfigurations(string $namespace, int $depth, int $flags, array $expected)
151+
{
152+
$tree = new PageTreeBuilder($namespace, $depth);
153+
if ($flags) {
154+
$tree->addFlag($flags);
155+
}
156+
$tree->generate();
157+
158+
$result = explode("\n", (string)$tree);
159+
sort($expected);
160+
sort($result);
161+
162+
$this->assertEquals($expected, $result);
163+
}
164+
165+
/**
166+
* This is the same test as above, but pretending that our data directory is in our test namespace.
167+
*
168+
* @dataProvider treeConfigProvider
169+
*/
170+
public function testTopLevelTree(string $namespace, int $depth, int $flags, array $expected)
171+
{
172+
global $conf;
173+
$conf['datadir'] .= '/namespace';
174+
175+
$expected = array_map(function ($item) use ($namespace) {
176+
return preg_replace('/namespace:?/', '', $item);
177+
}, $expected);
178+
179+
$namespace = '';
180+
$this->testPageTreeConfigurations($namespace, $depth, $flags, $expected);
181+
}
182+
183+
184+
public function testPageTreeLeaves()
185+
{
186+
$tree = new PageTreeBuilder('namespace');
187+
$tree->generate();
188+
189+
$leaves = $tree->getLeaves();
190+
$branches = $tree->getBranches();
191+
192+
// Test that we have both leaves and branches
193+
$this->assertGreaterThan(0, count($leaves), 'Should have leaf pages');
194+
$this->assertGreaterThan(0, count($branches), 'Should have branch pages');
195+
196+
// The total should equal all pages
197+
$this->assertEquals(count($tree->getAll()), count($leaves) + count($branches),
198+
'Leaves + branches should equal total pages');
199+
}
200+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
====== this is the Control ======
2+
3+
It has all kinds of [[:content]]. But also Links.
4+
5+
* This is not a link
6+
* [[this]] is
7+
* [[foo:bar]] is also a link
8+
* [[Another Link]]
9+
* [[https://www.google.com|External links]] are not lessons
10+
11+
We have two lists here
12+
13+
* [[foo:bar|duplicates]] will be ignored in the order
14+
* [[..:relativeup]]
15+
* [[foo2:this]]
16+
* [[foo2:deeper:item|Deeper Item]]
17+
* [[foo2:deeper:evendeeper:item|Even Deeper Item]]
18+
* [[.:blarg:down]]
19+
* [[:toplevel]]
20+
21+
Here is more and another [[link]].
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* [[briefs:start|Briefs]]
2+
* [[qhsr:start|QHSR]]
3+
* [[qhsr:q|Quality]]
4+
* [[qhsr:cert|Certification]]
5+
* [[qhsr:hse:start|HSE]]
6+
* [[qhsr:engsystems|Eng Systems]]
7+
* [[qhsr:performance|Eng Performance]]
8+
* [[qhsr:competence|Eng Competence]]
9+
* [[qhsr:ashford|Ashford DFO]]
10+
* [[qhsr:training|Technical Training]]
11+
* [[tech:start|Tech Info]]
12+
* [[https://homepage.company.com|Company Homepage]]

0 commit comments

Comments
 (0)