├── README.md ├── _test ├── query.test.php ├── query_operators.test.php ├── query_operators_numeric.test.php ├── query_operators_numeric_optional.test.php ├── query_sort.test.php ├── query_sort_optional.test.php ├── storage.test.php ├── storage_graphs.test.php ├── storage_removes.test.php ├── strataquerytest.inc.php ├── stratatest.inc.php └── types.test.php ├── action.php ├── aggregates ├── count.php ├── first.php ├── last.php ├── max.php ├── min.php ├── sum.php └── unique.php ├── conf ├── default.php └── metadata.php ├── driver ├── driver.php ├── mysql.php ├── pgsql.php └── sqlite.php ├── helper ├── syntax.php ├── triples.php └── util.php ├── images ├── sort_asc.png ├── sort_both.png └── sort_desc.png ├── lang ├── en │ ├── lang.php │ └── settings.php └── ja │ └── settings.php ├── lib ├── strata_aggregate.php ├── strata_exception.php ├── strata_querytree_visitor.php └── strata_type.php ├── manual.txt ├── plugin.info.txt ├── renderer.php ├── script.js ├── sql ├── setup-mysql.sql ├── setup-pgsql.sql └── setup-sqlite.sql ├── style.css ├── syntax ├── entry.php ├── info.php ├── list.php ├── nodata.php ├── select.php └── table.php └── types ├── date.php ├── image.php ├── link.php ├── page.php ├── ref.php ├── text.php └── wiki.php /README.md: -------------------------------------------------------------------------------- 1 | Strata 2 | ====== 3 | 4 | __This dokuwiki plugin is no longer maintained. Due to time constraints and no longer using this plugin myself, I can no longer properly maintain it.__ 5 | 6 | Strata is a semi-structured data plugin for [DokuWiki][dw]. Strata allows you to add data to your wiki pages, and later on query that data to create automatic indices or other overviews. 7 | 8 | #### Installation 9 | 1. Use the DokuWiki plugin manager to install Strata. 10 | 2. Optionally, copy the contents of the ``manual.txt`` file into a wiki page (``wiki:strata`` would be a good location) 11 | 3. Optionally, configure your DokuWiki instance's [`useheading`](https://www.dokuwiki.org/config:useheading) setting to have nicer-looking links. 12 | 13 | #### Quick-start 14 | Below is a very simple example of how to use Strata. You can find more information in the ``manual.txt`` (which you can copy-paste into a wiki page to have the manual available for all users). 15 | 16 | Add data to a page with: 17 | 18 | 19 | Full Name: John Doe 20 | Age: 24 21 | Contact [link]: john.doe@example.org 22 | 23 | 24 | Later on, you can make a list (to get a table, use ```` instead) of people with: 25 | 26 | 27 | ?person is a: person 28 | ?person Contact [link]: ?contact 29 | 30 | 31 | 32 | #### More Information 33 | 34 | See the [plugin page][pp] for more information on usage and configuration. 35 | 36 | [dw]: https://www.dokuwiki.org 37 | [pp]: https://www.dokuwiki.org/plugin:strata 38 | -------------------------------------------------------------------------------- /_test/query_operators.test.php: -------------------------------------------------------------------------------- 1 | 'select', 19 | 'grouping'=>array(), 20 | 'group' => array ( 21 | 'type' => 'filter', 22 | 'lhs' => array ( 23 | 'type' => 'triple', 24 | 'subject' => array ( 25 | 'type' => 'variable', 26 | 'text' => 'p' 27 | ), 28 | 'predicate' => array ( 29 | 'type' => 'literal', 30 | 'text' => 'tax rate' 31 | ), 32 | 'object' => array ( 33 | 'type' => 'variable', 34 | 'text' => 'tax' 35 | ) 36 | ), 37 | 'rhs' => array (array ( 38 | 'type' => 'operator', 39 | 'lhs' => array ( 40 | 'type' => 'variable', 41 | 'text' => 'tax' 42 | ), 43 | 'operator' => '=', 44 | 'rhs' => array ( 45 | 'type' => 'literal', 46 | 'text' => '2%' 47 | ) 48 | ) 49 | )), 50 | 'projection' => array ( 51 | 'p' 52 | ), 53 | 'ordering' => array ( 54 | array ( 55 | 'variable' => 'p', 56 | 'direction' => 'asc' 57 | ) 58 | ) 59 | ); 60 | 61 | $expected = array ( 62 | array ( 63 | 'p' => array('person:carol') 64 | ) 65 | ); 66 | 67 | $this->assertQueryResult($query, $expected); 68 | } 69 | 70 | function testNotEquals() { 71 | $query = array ( 72 | 'type' => 'select', 73 | 'grouping'=>array(), 74 | 'group' => array ( 75 | 'type' => 'filter', 76 | 'lhs' => array ( 77 | 'type' => 'triple', 78 | 'subject' => array ( 79 | 'type' => 'variable', 80 | 'text' => 'p' 81 | ), 82 | 'predicate' => array ( 83 | 'type' => 'literal', 84 | 'text' => 'tax rate' 85 | ), 86 | 'object' => array ( 87 | 'type' => 'variable', 88 | 'text' => 'tax' 89 | ) 90 | ), 91 | 'rhs' => array (array ( 92 | 'type' => 'operator', 93 | 'lhs' => array ( 94 | 'type' => 'variable', 95 | 'text' => 'tax' 96 | ), 97 | 'operator' => '!=', 98 | 'rhs' => array ( 99 | 'type' => 'literal', 100 | 'text' => '2%' 101 | ) 102 | ) 103 | )), 104 | 'projection' => array ( 105 | 'p' 106 | ), 107 | 'ordering' => array ( 108 | array ( 109 | 'variable' => 'p', 110 | 'direction' => 'asc' 111 | ) 112 | ) 113 | ); 114 | 115 | $expected = array ( 116 | array ( 117 | 'p' => array('person:alice') 118 | ), 119 | array ( 120 | 'p' => array('person:bob') 121 | ) 122 | ); 123 | 124 | $this->assertQueryResult($query, $expected); 125 | } 126 | 127 | function testLike() { 128 | $query = array ( 129 | 'type' => 'select', 130 | 'grouping'=>array(), 131 | 'group' => array ( 132 | 'type' => 'filter', 133 | 'lhs' => array ( 134 | 'type' => 'triple', 135 | 'subject' => array ( 136 | 'type' => 'variable', 137 | 'text' => 'p' 138 | ), 139 | 'predicate' => array ( 140 | 'type' => 'literal', 141 | 'text' => 'tax rate' 142 | ), 143 | 'object' => array ( 144 | 'type' => 'variable', 145 | 'text' => 'tax' 146 | ) 147 | ), 148 | 'rhs' => array (array ( 149 | 'type' => 'operator', 150 | 'lhs' => array ( 151 | 'type' => 'variable', 152 | 'text' => 'tax' 153 | ), 154 | 'operator' => '~', 155 | 'rhs' => array ( 156 | 'type' => 'literal', 157 | 'text' => '2%' 158 | ) 159 | ) 160 | )), 161 | 'projection' => array ( 162 | 'p' 163 | ), 164 | 'ordering' => array ( 165 | array ( 166 | 'variable' => 'p', 167 | 'direction' => 'asc' 168 | ) 169 | ) 170 | ); 171 | 172 | $expected = array ( 173 | array ( 174 | 'p' => array('person:carol') 175 | ) 176 | ); 177 | 178 | $this->assertQueryResult($query, $expected); 179 | } 180 | 181 | function testNotLike() { 182 | $query = array ( 183 | 'type' => 'select', 184 | 'grouping'=>array(), 185 | 'group' => array ( 186 | 'type' => 'filter', 187 | 'lhs' => array ( 188 | 'type' => 'triple', 189 | 'subject' => array ( 190 | 'type' => 'variable', 191 | 'text' => 'p' 192 | ), 193 | 'predicate' => array ( 194 | 'type' => 'literal', 195 | 'text' => 'tax rate' 196 | ), 197 | 'object' => array ( 198 | 'type' => 'variable', 199 | 'text' => 'tax' 200 | ) 201 | ), 202 | 'rhs' => array (array ( 203 | 'type' => 'operator', 204 | 'lhs' => array ( 205 | 'type' => 'variable', 206 | 'text' => 'tax' 207 | ), 208 | 'operator' => '!~', 209 | 'rhs' => array ( 210 | 'type' => 'literal', 211 | 'text' => '2%' 212 | ) 213 | ) 214 | )), 215 | 'projection' => array ( 216 | 'p' 217 | ), 218 | 'ordering' => array ( 219 | array ( 220 | 'variable' => 'p', 221 | 'direction' => 'asc' 222 | ) 223 | ) 224 | ); 225 | 226 | $expected = array ( 227 | array ( 228 | 'p' => array('person:alice') 229 | ), 230 | array ( 231 | 'p' => array('person:bob') 232 | ) 233 | ); 234 | 235 | $this->assertQueryResult($query, $expected); 236 | } 237 | 238 | function testBeginsWith() { 239 | $query = array ( 240 | 'type' => 'select', 241 | 'grouping'=>array(), 242 | 'group' => array ( 243 | 'type' => 'filter', 244 | 'lhs' => array ( 245 | 'type' => 'triple', 246 | 'subject' => array ( 247 | 'type' => 'variable', 248 | 'text' => 'p' 249 | ), 250 | 'predicate' => array ( 251 | 'type' => 'literal', 252 | 'text' => 'tax rate' 253 | ), 254 | 'object' => array ( 255 | 'type' => 'variable', 256 | 'text' => 'tax' 257 | ) 258 | ), 259 | 'rhs' => array (array ( 260 | 'type' => 'operator', 261 | 'lhs' => array ( 262 | 'type' => 'variable', 263 | 'text' => 'tax' 264 | ), 265 | 'operator' => '^~', 266 | 'rhs' => array ( 267 | 'type' => 'literal', 268 | 'text' => '2%' 269 | ) 270 | ) 271 | )), 272 | 'projection' => array ( 273 | 'p' 274 | ), 275 | 'ordering' => array ( 276 | array ( 277 | 'variable' => 'p', 278 | 'direction' => 'asc' 279 | ) 280 | ) 281 | ); 282 | 283 | $expected = array ( 284 | array ( 285 | 'p' => array('person:carol') 286 | ) 287 | ); 288 | 289 | $this->assertQueryResult($query, $expected); 290 | } 291 | 292 | function testEndsWith() { 293 | $query = array ( 294 | 'type' => 'select', 295 | 'grouping'=>array(), 296 | 'group' => array ( 297 | 'type' => 'filter', 298 | 'lhs' => array ( 299 | 'type' => 'triple', 300 | 'subject' => array ( 301 | 'type' => 'variable', 302 | 'text' => 'p' 303 | ), 304 | 'predicate' => array ( 305 | 'type' => 'literal', 306 | 'text' => 'tax rate' 307 | ), 308 | 'object' => array ( 309 | 'type' => 'variable', 310 | 'text' => 'tax' 311 | ) 312 | ), 313 | 'rhs' => array (array ( 314 | 'type' => 'operator', 315 | 'lhs' => array ( 316 | 'type' => 'variable', 317 | 'text' => 'tax' 318 | ), 319 | 'operator' => '$~', 320 | 'rhs' => array ( 321 | 'type' => 'literal', 322 | 'text' => '2%' 323 | ) 324 | ) 325 | )), 326 | 'projection' => array ( 327 | 'p' 328 | ), 329 | 'ordering' => array ( 330 | array ( 331 | 'variable' => 'p', 332 | 'direction' => 'asc' 333 | ) 334 | ) 335 | ); 336 | 337 | $expected = array ( 338 | array ( 339 | 'p' => array('person:carol') 340 | ) 341 | ); 342 | 343 | $this->assertQueryResult($query, $expected); 344 | } 345 | 346 | } 347 | 348 | -------------------------------------------------------------------------------- /_test/query_operators_numeric.test.php: -------------------------------------------------------------------------------- 1 | 'select', 19 | 'grouping'=>array(), 20 | 'group' => array ( 21 | 'type' => 'filter', 22 | 'lhs' => array ( 23 | 'type' => 'triple', 24 | 'subject' => array ( 25 | 'type' => 'variable', 26 | 'text' => 'p' 27 | ), 28 | 'predicate' => array ( 29 | 'type' => 'literal', 30 | 'text' => 'is rated' 31 | ), 32 | 'object' => array ( 33 | 'type' => 'variable', 34 | 'text' => 'rating' 35 | ) 36 | ), 37 | 'rhs' => array ( 38 | array ( 39 | 'type' => 'operator', 40 | 'lhs' => array ( 41 | 'type' => 'variable', 42 | 'text' => 'rating' 43 | ), 44 | 'operator' => '>', 45 | 'rhs' => array ( 46 | 'type' => 'literal', 47 | 'text' => '1' 48 | ) 49 | ), 50 | array ( 51 | 'type' => 'operator', 52 | 'lhs' => array ( 53 | 'type' => 'variable', 54 | 'text' => 'rating' 55 | ), 56 | 'operator' => '<=', 57 | 'rhs' => array ( 58 | 'type' => 'literal', 59 | 'text' => '10' 60 | ) 61 | ) 62 | )), 63 | 'projection' => array ( 64 | 'p', 65 | 'rating' 66 | ), 67 | 'ordering' => array ( 68 | array ( 69 | 'variable' => 'p', 70 | 'direction' => 'asc' 71 | ) 72 | ) 73 | ); 74 | 75 | $expected = array ( 76 | array ( 77 | 'p' => array('person:alice'), 78 | 'rating' => array('10') 79 | ), 80 | array ( 81 | 'p' => array('person:bob'), 82 | 'rating' => array('8') 83 | ) 84 | ); 85 | 86 | $this->assertQueryResult($query, $expected); 87 | } 88 | 89 | function testGteLt() { 90 | $query = array ( 91 | 'type' => 'select', 92 | 'grouping'=>array(), 93 | 'group' => array ( 94 | 'type' => 'filter', 95 | 'lhs' => array ( 96 | 'type' => 'triple', 97 | 'subject' => array ( 98 | 'type' => 'variable', 99 | 'text' => 'p' 100 | ), 101 | 'predicate' => array ( 102 | 'type' => 'literal', 103 | 'text' => 'is rated' 104 | ), 105 | 'object' => array ( 106 | 'type' => 'variable', 107 | 'text' => 'rating' 108 | ) 109 | ), 110 | 'rhs' => array ( 111 | array ( 112 | 'type' => 'operator', 113 | 'lhs' => array ( 114 | 'type' => 'variable', 115 | 'text' => 'rating' 116 | ), 117 | 'operator' => '>=', 118 | 'rhs' => array ( 119 | 'type' => 'literal', 120 | 'text' => '1' 121 | ) 122 | ), 123 | array ( 124 | 'type' => 'operator', 125 | 'lhs' => array ( 126 | 'type' => 'variable', 127 | 'text' => 'rating' 128 | ), 129 | 'operator' => '<', 130 | 'rhs' => array ( 131 | 'type' => 'literal', 132 | 'text' => '10' 133 | ) 134 | ) 135 | )), 136 | 'projection' => array ( 137 | 'p', 138 | 'rating' 139 | ), 140 | 'ordering' => array ( 141 | array ( 142 | 'variable' => 'p', 143 | 'direction' => 'asc' 144 | ) 145 | ) 146 | ); 147 | 148 | $expected = array ( 149 | array ( 150 | 'p' => array('person:bob'), 151 | 'rating' => array('8') 152 | ), 153 | array ( 154 | 'p' => array('person:carol'), 155 | 'rating' => array('1') 156 | ) 157 | ); 158 | 159 | $this->assertQueryResult($query, $expected); 160 | } 161 | 162 | function testPartiallyNumeric() { 163 | $query = array ( 164 | 'type' => 'select', 165 | 'grouping'=>array(), 166 | 'group' => array ( 167 | 'type' => 'filter', 168 | 'lhs' => array ( 169 | 'type' => 'triple', 170 | 'subject' => array ( 171 | 'type' => 'variable', 172 | 'text' => 'p' 173 | ), 174 | 'predicate' => array ( 175 | 'type' => 'literal', 176 | 'text' => 'tax rate' 177 | ), 178 | 'object' => array ( 179 | 'type' => 'variable', 180 | 'text' => 'tax' 181 | ) 182 | ), 183 | 'rhs' => array ( 184 | array ( 185 | 'type' => 'operator', 186 | 'lhs' => array ( 187 | 'type' => 'variable', 188 | 'text' => 'tax' 189 | ), 190 | 'operator' => '>', 191 | 'rhs' => array ( 192 | 'type' => 'literal', 193 | 'text' => '2' 194 | ) 195 | ), 196 | array ( 197 | 'type' => 'operator', 198 | 'lhs' => array ( 199 | 'type' => 'variable', 200 | 'text' => 'tax' 201 | ), 202 | 'operator' => '<=', 203 | 'rhs' => array ( 204 | 'type' => 'literal', 205 | 'text' => '15' 206 | ) 207 | ) 208 | )), 209 | 'projection' => array ( 210 | 'p', 211 | 'tax' 212 | ), 213 | 'ordering' => array ( 214 | array ( 215 | 'variable' => 'p', 216 | 'direction' => 'asc' 217 | ) 218 | ) 219 | ); 220 | 221 | // Result might vary depending on database backed, only require that it does not fail 222 | $this->assertTrue($this->_triples->queryRelations($query)); 223 | } 224 | 225 | function testNonNumeric() { 226 | $query = array ( 227 | 'type' => 'select', 228 | 'grouping'=>array(), 229 | 'group' => array ( 230 | 'type' => 'filter', 231 | 'lhs' => array ( 232 | 'type' => 'triple', 233 | 'subject' => array ( 234 | 'type' => 'variable', 235 | 'text' => 'p' 236 | ), 237 | 'predicate' => array ( 238 | 'type' => 'literal', 239 | 'text' => 'class' 240 | ), 241 | 'object' => array ( 242 | 'type' => 'variable', 243 | 'text' => 'person' 244 | ) 245 | ), 246 | 'rhs' => array ( 247 | array ( 248 | 'type' => 'operator', 249 | 'lhs' => array ( 250 | 'type' => 'variable', 251 | 'text' => 'p' 252 | ), 253 | 'operator' => '>', 254 | 'rhs' => array ( 255 | 'type' => 'literal', 256 | 'text' => '2' 257 | ) 258 | ), 259 | array ( 260 | 'type' => 'operator', 261 | 'lhs' => array ( 262 | 'type' => 'variable', 263 | 'text' => 'p' 264 | ), 265 | 'operator' => '<=', 266 | 'rhs' => array ( 267 | 'type' => 'literal', 268 | 'text' => '25' 269 | ) 270 | ) 271 | )), 272 | 'projection' => array ( 273 | 'p' 274 | ), 275 | 'ordering' => array ( 276 | array ( 277 | 'variable' => 'p', 278 | 'direction' => 'asc' 279 | ) 280 | ) 281 | ); 282 | 283 | // Result might vary depending on database backed, only require that it does not fail 284 | $this->assertTrue($this->_triples->queryRelations($query)); 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /_test/query_operators_numeric_optional.test.php: -------------------------------------------------------------------------------- 1 | 'select', 19 | 'grouping'=>array(), 20 | 'group' => array ( 21 | 'type' => 'filter', 22 | 'lhs' => array ( 23 | 'type' => 'triple', 24 | 'subject' => array ( 25 | 'type' => 'variable', 26 | 'text' => 'p' 27 | ), 28 | 'predicate' => array ( 29 | 'type' => 'literal', 30 | 'text' => 'tax rate' 31 | ), 32 | 'object' => array ( 33 | 'type' => 'variable', 34 | 'text' => 'tax' 35 | ) 36 | ), 37 | 'rhs' => array ( 38 | array ( 39 | 'type' => 'operator', 40 | 'lhs' => array ( 41 | 'type' => 'variable', 42 | 'text' => 'tax' 43 | ), 44 | 'operator' => '>', 45 | 'rhs' => array ( 46 | 'type' => 'literal', 47 | 'text' => '2' 48 | ) 49 | ), 50 | array ( 51 | 'type' => 'operator', 52 | 'lhs' => array ( 53 | 'type' => 'variable', 54 | 'text' => 'tax' 55 | ), 56 | 'operator' => '<=', 57 | 'rhs' => array ( 58 | 'type' => 'literal', 59 | 'text' => '25' 60 | ) 61 | ) 62 | )), 63 | 'projection' => array ( 64 | 'p', 65 | 'tax' 66 | ), 67 | 'ordering' => array ( 68 | array ( 69 | 'variable' => 'p', 70 | 'direction' => 'asc' 71 | ) 72 | ) 73 | ); 74 | 75 | $expected = array ( 76 | array ( 77 | 'p' => array('person:alice'), 78 | 'tax' => array('10%') 79 | ), 80 | array ( 81 | 'p' => array('person:bob'), 82 | 'tax' => array('25%') 83 | ) 84 | ); 85 | 86 | $this->assertQueryResult($query, $expected, 'Partial numeric comparison (first numbers, then text) unsupported'); 87 | } 88 | 89 | function testGteLtPartiallyNumeric() { 90 | $query = array ( 91 | 'type' => 'select', 92 | 'grouping'=>array(), 93 | 'group' => array ( 94 | 'type' => 'filter', 95 | 'lhs' => array ( 96 | 'type' => 'triple', 97 | 'subject' => array ( 98 | 'type' => 'variable', 99 | 'text' => 'p' 100 | ), 101 | 'predicate' => array ( 102 | 'type' => 'literal', 103 | 'text' => 'tax rate' 104 | ), 105 | 'object' => array ( 106 | 'type' => 'variable', 107 | 'text' => 'tax' 108 | ) 109 | ), 110 | 'rhs' => array ( 111 | array ( 112 | 'type' => 'operator', 113 | 'lhs' => array ( 114 | 'type' => 'variable', 115 | 'text' => 'tax' 116 | ), 117 | 'operator' => '>=', 118 | 'rhs' => array ( 119 | 'type' => 'literal', 120 | 'text' => '2' 121 | ) 122 | ), 123 | array ( 124 | 'type' => 'operator', 125 | 'lhs' => array ( 126 | 'type' => 'variable', 127 | 'text' => 'tax' 128 | ), 129 | 'operator' => '<', 130 | 'rhs' => array ( 131 | 'type' => 'literal', 132 | 'text' => '25' 133 | ) 134 | ) 135 | )), 136 | 'projection' => array ( 137 | 'p', 138 | 'tax' 139 | ), 140 | 'ordering' => array ( 141 | array ( 142 | 'variable' => 'p', 143 | 'direction' => 'asc' 144 | ) 145 | ) 146 | ); 147 | 148 | $expected = array ( 149 | array ( 150 | 'p' => array('person:alice'), 151 | 'tax' => array('10%') 152 | ), 153 | array ( 154 | 'p' => array('person:carol'), 155 | 'tax' => array('2%') 156 | ) 157 | ); 158 | 159 | $this->assertQueryResult($query, $expected, 'Partial numeric comparison (first numbers, then text) unsupported'); 160 | } 161 | 162 | function testGtLteNatural() { 163 | $query = array ( 164 | 'type' => 'select', 165 | 'grouping'=>array(), 166 | 'group' => array ( 167 | 'type' => 'filter', 168 | 'lhs' => array ( 169 | 'type' => 'triple', 170 | 'subject' => array ( 171 | 'type' => 'variable', 172 | 'text' => 'p' 173 | ), 174 | 'predicate' => array ( 175 | 'type' => 'literal', 176 | 'text' => 'has length' 177 | ), 178 | 'object' => array ( 179 | 'type' => 'variable', 180 | 'text' => 'length' 181 | ) 182 | ), 183 | 'rhs' => array ( 184 | array ( 185 | 'type' => 'operator', 186 | 'lhs' => array ( 187 | 'type' => 'variable', 188 | 'text' => 'length' 189 | ), 190 | 'operator' => '>', 191 | 'rhs' => array ( 192 | 'type' => 'literal', 193 | 'text' => '4 ft' 194 | ) 195 | ), 196 | array ( 197 | 'type' => 'operator', 198 | 'lhs' => array ( 199 | 'type' => 'variable', 200 | 'text' => 'length' 201 | ), 202 | 'operator' => '<=', 203 | 'rhs' => array ( 204 | 'type' => 'literal', 205 | 'text' => '5 ft 5 in' 206 | ) 207 | ) 208 | )), 209 | 'projection' => array ( 210 | 'p', 211 | 'length' 212 | ), 213 | 'ordering' => array ( 214 | array ( 215 | 'variable' => 'p', 216 | 'direction' => 'asc' 217 | ) 218 | ) 219 | ); 220 | 221 | $expected = array ( 222 | array ( 223 | 'p' => array('person:alice'), 224 | 'length' => array('5 ft 5 in') 225 | ), 226 | array ( 227 | 'p' => array('person:carol'), 228 | 'length' => array('4 ft 11 in') 229 | ) 230 | ); 231 | 232 | $this->assertQueryResult($query, $expected, 'Natural comparison unsupported'); 233 | } 234 | 235 | function testGteLtNatural() { 236 | $query = array ( 237 | 'type' => 'select', 238 | 'grouping'=>array(), 239 | 'group' => array ( 240 | 'type' => 'filter', 241 | 'lhs' => array ( 242 | 'type' => 'triple', 243 | 'subject' => array ( 244 | 'type' => 'variable', 245 | 'text' => 'p' 246 | ), 247 | 'predicate' => array ( 248 | 'type' => 'literal', 249 | 'text' => 'has length' 250 | ), 251 | 'object' => array ( 252 | 'type' => 'variable', 253 | 'text' => 'length' 254 | ) 255 | ), 256 | 'rhs' => array ( 257 | array ( 258 | 'type' => 'operator', 259 | 'lhs' => array ( 260 | 'type' => 'variable', 261 | 'text' => 'length' 262 | ), 263 | 'operator' => '>=', 264 | 'rhs' => array ( 265 | 'type' => 'literal', 266 | 'text' => '4 ft' 267 | ) 268 | ), 269 | array ( 270 | 'type' => 'operator', 271 | 'lhs' => array ( 272 | 'type' => 'variable', 273 | 'text' => 'length' 274 | ), 275 | 'operator' => '<', 276 | 'rhs' => array ( 277 | 'type' => 'literal', 278 | 'text' => '5 ft 10 in' 279 | ) 280 | ) 281 | )), 282 | 'projection' => array ( 283 | 'p', 284 | 'length' 285 | ), 286 | 'ordering' => array ( 287 | array ( 288 | 'variable' => 'p', 289 | 'direction' => 'asc' 290 | ) 291 | ) 292 | ); 293 | 294 | $expected = array ( 295 | array ( 296 | 'p' => array('person:alice'), 297 | 'length' => array('5 ft 5 in') 298 | ), 299 | array ( 300 | 'p' => array('person:carol'), 301 | 'length' => array('4 ft 11 in') 302 | ) 303 | ); 304 | 305 | $this->assertQueryResult($query, $expected, 'Natural comparison unsupported'); 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /_test/query_sort.test.php: -------------------------------------------------------------------------------- 1 | 'select', 19 | 'grouping'=>array(), 20 | 'group' => array ( 21 | 'type' => 'triple', 22 | 'subject' => array ( 23 | 'type' => 'variable', 24 | 'text' => 'p' 25 | ), 26 | 'predicate' => array ( 27 | 'type' => 'literal', 28 | 'text' => 'is rated' 29 | ), 30 | 'object' => array ( 31 | 'type' => 'variable', 32 | 'text' => 'rating' 33 | ) 34 | ), 35 | 'projection' => array ( 36 | 'p', 37 | 'rating' 38 | ), 39 | 'ordering' => array ( 40 | array ( 41 | 'variable' => 'rating', 42 | 'direction' => 'asc' 43 | ) 44 | ) 45 | ); 46 | 47 | $expected = array ( 48 | array ( 49 | 'p' => array('person:carol'), 50 | 'rating' => array('1') 51 | ), 52 | array ( 53 | 'p' => array('person:bob'), 54 | 'rating' => array('8') 55 | ), 56 | array ( 57 | 'p' => array('person:alice'), 58 | 'rating' => array('10') 59 | ) 60 | ); 61 | 62 | $this->assertQueryResult($query, $expected); 63 | } 64 | 65 | function testPartiallyNumericSort() { 66 | $query = array ( 67 | 'type' => 'select', 68 | 'grouping'=>array(), 69 | 'group' => array ( 70 | 'type' => 'triple', 71 | 'subject' => array ( 72 | 'type' => 'variable', 73 | 'text' => 'p' 74 | ), 75 | 'predicate' => array ( 76 | 'type' => 'literal', 77 | 'text' => 'tax rate' 78 | ), 79 | 'object' => array ( 80 | 'type' => 'variable', 81 | 'text' => 'tax' 82 | ) 83 | ), 84 | 'projection' => array ( 85 | 'p', 86 | 'tax' 87 | ), 88 | 'ordering' => array ( 89 | array ( 90 | 'variable' => 'tax', 91 | 'direction' => 'asc' 92 | ) 93 | ) 94 | ); 95 | 96 | // Result might vary depending on database backed, only require that it does not fail 97 | $this->assertTrue($this->_triples->queryRelations($query)); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /_test/query_sort_optional.test.php: -------------------------------------------------------------------------------- 1 | 'select', 19 | 'grouping'=>array(), 20 | 'group' => array ( 21 | 'type' => 'triple', 22 | 'subject' => array ( 23 | 'type' => 'variable', 24 | 'text' => 'p' 25 | ), 26 | 'predicate' => array ( 27 | 'type' => 'literal', 28 | 'text' => 'tax rate' 29 | ), 30 | 'object' => array ( 31 | 'type' => 'variable', 32 | 'text' => 'tax' 33 | ) 34 | ), 35 | 'projection' => array ( 36 | 'p', 37 | 'tax' 38 | ), 39 | 'ordering' => array ( 40 | array ( 41 | 'variable' => 'tax', 42 | 'direction' => 'asc' 43 | ) 44 | ) 45 | ); 46 | 47 | $expected = array ( 48 | array ( 49 | 'p' => array('person:carol'), 50 | 'tax' => array('2%') 51 | ), 52 | array ( 53 | 'p' => array('person:alice'), 54 | 'tax' => array('10%') 55 | ), 56 | array ( 57 | 'p' => array('person:bob'), 58 | 'tax' => array('25%') 59 | ) 60 | ); 61 | 62 | $this->assertQueryResult($query, $expected, 'Partial numeric sort (numbers first, text second) unsupported'); 63 | } 64 | 65 | function testNaturalSort() { 66 | $query = array ( 67 | 'type' => 'select', 68 | 'grouping'=>array(), 69 | 'group' => array ( 70 | 'type' => 'triple', 71 | 'subject' => array ( 72 | 'type' => 'variable', 73 | 'text' => 'p' 74 | ), 75 | 'predicate' => array ( 76 | 'type' => 'literal', 77 | 'text' => 'has length' 78 | ), 79 | 'object' => array ( 80 | 'type' => 'variable', 81 | 'text' => 'length' 82 | ) 83 | ), 84 | 'projection' => array ( 85 | 'p', 86 | 'length' 87 | ), 88 | 'ordering' => array ( 89 | array ( 90 | 'variable' => 'length', 91 | 'direction' => 'asc' 92 | ) 93 | ) 94 | ); 95 | 96 | $expected = array ( 97 | array ( 98 | 'p' => array('person:carol'), 99 | 'length' => array('4 ft 11 in') 100 | ), 101 | array ( 102 | 'p' => array('person:alice'), 103 | 'length' => array('5 ft 5 in') 104 | ), 105 | array ( 106 | 'p' => array('person:bob'), 107 | 'length' => array('5 ft 10 in') 108 | ) 109 | ); 110 | 111 | $this->assertQueryResult($query, $expected, 'Full natural sort unsupported'); 112 | } 113 | 114 | function testUnicodeSort() { 115 | $query = array ( 116 | 'type' => 'select', 117 | 'grouping'=>array(), 118 | 'group' => array ( 119 | 'type' => 'triple', 120 | 'subject' => array ( 121 | 'type' => 'variable', 122 | 'text' => 'p' 123 | ), 124 | 'predicate' => array ( 125 | 'type' => 'literal', 126 | 'text' => 'identifier' 127 | ), 128 | 'object' => array ( 129 | 'type' => 'variable', 130 | 'text' => 'id' 131 | ) 132 | ), 133 | 'projection' => array ( 134 | 'p', 135 | 'id' 136 | ), 137 | 'ordering' => array ( 138 | array ( 139 | 'variable' => 'id', 140 | 'direction' => 'asc' 141 | ) 142 | ) 143 | ); 144 | 145 | $expected = array ( 146 | array ( 147 | 'p' => array('person:alice'), 148 | 'id' => array('α') 149 | ), 150 | array ( 151 | 'p' => array('person:bob'), 152 | 'id' => array('Β') 153 | ), 154 | array ( 155 | 'p' => array('person:carol'), 156 | 'id' => array('γ') 157 | ) 158 | ); 159 | 160 | $this->assertQueryResult($query, $expected, 'Case insensitive unicode sort unsupported'); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /_test/storage.test.php: -------------------------------------------------------------------------------- 1 | _triples->addTriple('Bob', 'knows', 'Alice', 'wiki'); 15 | $this->assertTrue($OK); 16 | $data = $this->_triples->fetchTriples(); 17 | $expected = array( 18 | array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice', 'graph' => 'wiki') 19 | ); 20 | $this->assertEquals($data, $expected); 21 | } 22 | 23 | function testAddArray() { 24 | $OK = $this->_triples->addTriples(array(array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice')), 'wiki'); 25 | $this->assertTrue($OK); 26 | $data = $this->_triples->fetchTriples(); 27 | $expected = array( 28 | array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice', 'graph' => 'wiki') 29 | ); 30 | $this->assertEquals($data, $expected); 31 | } 32 | 33 | function testAddMulti() { 34 | $OK = $this->_triples->addTriple('Bob', 'knows', 'Alice', 'wiki'); 35 | $this->assertTrue($OK); 36 | $OK =$this->_triples->addTriple('Alice', 'knows', 'Carol', 'wiki'); 37 | $this->assertTrue($OK); 38 | $data = $this->_triples->fetchTriples(); 39 | $expected = array( 40 | array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice', 'graph' => 'wiki'), 41 | array('subject' => 'Alice', 'predicate' => 'knows', 'object' => 'Carol', 'graph' => 'wiki') 42 | ); 43 | $this->assertEquals($data, $expected); 44 | } 45 | 46 | function testSpecialChars() { 47 | $OK = $this->_triples->addTriple('*', 'select', '%', 'wiki'); 48 | $this->assertTrue($OK); 49 | $OK =$this->_triples->addTriple('_', '(', '`', 'wiki'); 50 | $this->assertTrue($OK); 51 | $OK =$this->_triples->addTriple(';', '\'', '"', 'wiki'); 52 | $this->assertTrue($OK); 53 | $data = $this->_triples->fetchTriples(); 54 | $expected = array( 55 | array('subject' => '*', 'predicate' => 'select', 'object' => '%', 'graph' => 'wiki'), 56 | array('subject' => '_', 'predicate' => '(', 'object' => '`', 'graph' => 'wiki'), 57 | array('subject' => ';', 'predicate' => '\'', 'object' => '"', 'graph' => 'wiki') 58 | ); 59 | $this->assertEquals($data, $expected); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /_test/storage_graphs.test.php: -------------------------------------------------------------------------------- 1 | _triples->addTriple('Bob', 'knows', 'Alice', 'knowledgebase of bob'); 15 | $this->assertTrue($OK); 16 | $OK = $this->_triples->addTriple('Alice', 'knows', 'Carol', 'knowledgebase of alice'); 17 | $this->assertTrue($OK); 18 | 19 | $expected1 = array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice', 'graph' => 'knowledgebase of bob'); 20 | $expected2 = array('subject' => 'Alice', 'predicate' => 'knows', 'object' => 'Carol', 'graph' => 'knowledgebase of alice'); 21 | 22 | // Retrieve the wiki graph 23 | $data = $this->_triples->fetchTriples(null, null, null, 'wiki'); 24 | $this->assertEquals($data, array()); 25 | 26 | // Retrieve Bobs graph 27 | $data = $this->_triples->fetchTriples(null, null, null, 'knowledgebase of bob'); 28 | $this->assertEquals($data, array($expected1)); 29 | 30 | // Retrieve Alices graph 31 | $data = $this->_triples->fetchTriples(null, null, null, 'knowledgebase of alice'); 32 | $this->assertEquals($data, array($expected2)); 33 | 34 | // Retrieve all graphs 35 | $data = $this->_triples->fetchTriples(null, null, null, null); 36 | $this->assertEquals($data, array($expected1, $expected2)); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /_test/storage_removes.test.php: -------------------------------------------------------------------------------- 1 | _triples->addTriple('Bob', 'knows', 'Alice', 'wiki'); 17 | $this->assertTrue($OK); 18 | $OK =$this->_triples->addTriple('Alice', 'knows', 'Carol', 'wiki'); 19 | $this->assertTrue($OK); 20 | $OK =$this->_triples->addTriple('Alice', 'dislikes', 'Carol', 'wiki'); 21 | $this->assertTrue($OK); 22 | 23 | $this->expected1 = array('subject' => 'Bob', 'predicate' => 'knows', 'object' => 'Alice', 'graph' => 'wiki'); 24 | $this->expected2 = array('subject' => 'Alice', 'predicate' => 'knows', 'object' => 'Carol', 'graph' => 'wiki'); 25 | $this->expected3 = array('subject' => 'Alice', 'predicate' => 'dislikes', 'object' => 'Carol', 'graph' => 'wiki'); 26 | 27 | $data = $this->_triples->fetchTriples(); 28 | $this->assertEquals($data, array($this->expected1, $this->expected2, $this->expected3)); 29 | } 30 | 31 | function testRemove() { 32 | // Remove all 33 | $this->_triples->removeTriples(); 34 | $data = $this->_triples->fetchTriples(); 35 | $this->assertEquals($data, array()); 36 | } 37 | 38 | function testRemoveBySubject() { 39 | $this->_triples->removeTriples('Bob'); 40 | $data = $this->_triples->fetchTriples(); 41 | $this->assertEquals($data, array($this->expected2, $this->expected3)); 42 | 43 | $this->_triples->removeTriples('Alice'); 44 | $data = $this->_triples->fetchTriples(); 45 | $this->assertEquals($data, array()); 46 | } 47 | 48 | function testRemoveByPredicate() { 49 | $this->_triples->removeTriples(null, 'knows'); 50 | $data = $this->_triples->fetchTriples(); 51 | $this->assertEquals($data, array($this->expected3)); 52 | 53 | $this->_triples->removeTriples(null, 'dislikes'); 54 | $data = $this->_triples->fetchTriples(); 55 | $this->assertEquals($data, array()); 56 | } 57 | 58 | function testRemoveByObject() { 59 | $this->_triples->removeTriples(null, null, 'Alice'); 60 | $data = $this->_triples->fetchTriples(); 61 | $this->assertEquals($data, array($this->expected2, $this->expected3)); 62 | 63 | $this->_triples->removeTriples(null, null, 'Carol'); 64 | $data = $this->_triples->fetchTriples(); 65 | $this->assertEquals($data, array()); 66 | } 67 | 68 | function testRemoveBySubjectAndPredicate() { 69 | $this->_triples->removeTriples('Alice', 'knows'); 70 | $data = $this->_triples->fetchTriples(); 71 | $this->assertEquals($data, array($this->expected1, $this->expected3)); 72 | } 73 | 74 | function testRemoveByPredicateAndObject() { 75 | $this->_triples->removeTriples(null, 'knows', 'Carol'); 76 | $data = $this->_triples->fetchTriples(); 77 | $this->assertEquals($data, array($this->expected1, $this->expected3)); 78 | } 79 | 80 | function testRemoveCaseInsensitive() { 81 | $this->_triples->removeTriples('bob'); 82 | $data = $this->_triples->fetchTriples(); 83 | $this->assertEquals($data, array($this->expected2, $this->expected3)); 84 | 85 | $this->_triples->removeTriples(null, 'Knows'); 86 | $data = $this->_triples->fetchTriples(); 87 | $this->assertEquals($data, array($this->expected3)); 88 | 89 | $this->_triples->removeTriples(null, null, 'carol'); 90 | $data = $this->_triples->fetchTriples(); 91 | $this->assertEquals($data, array()); 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /_test/strataquerytest.inc.php: -------------------------------------------------------------------------------- 1 | loadType('string'); 18 | $ref = $types->loadType('ref'); 19 | $image = $types->loadType('image'); 20 | 21 | // Create objects 22 | $bob = $ref->normalize('Bob', 'person'); 23 | $alice = $ref->normalize('Alice', 'person'); 24 | $carol = $ref->normalize('Carol', 'person'); 25 | 26 | $img_bob = $ref->normalize('Bob.png', 50); 27 | $img_alice = $ref->normalize('Alice.svg', 50); 28 | $img_carol = $ref->normalize('Carol.jpg', 50); 29 | 30 | // Fill database 31 | $this->_triples->addTriple($bob, 'class', 'person', 'wiki'); 32 | $this->_triples->addTriple($alice, 'class', 'person', 'wiki'); 33 | $this->_triples->addTriple($carol, 'class', 'person', 'wiki'); 34 | 35 | $this->_triples->addTriple($bob, 'name', 'Bob', 'wiki'); 36 | $this->_triples->addTriple($alice, 'name', 'Alice', 'wiki'); 37 | $this->_triples->addTriple($carol, 'name', 'Carol', 'wiki'); 38 | 39 | $this->_triples->addTriple($bob, 'identifier', 'Β', 'wiki'); 40 | $this->_triples->addTriple($alice, 'identifier', 'α', 'wiki'); 41 | $this->_triples->addTriple($carol, 'identifier', 'γ', 'wiki'); 42 | 43 | $this->_triples->addTriple($bob, 'knows', $alice, 'wiki'); 44 | $this->_triples->addTriple($alice, 'knows', $carol, 'wiki'); 45 | $this->_triples->addTriple($carol, 'knows', $bob, 'wiki'); 46 | $this->_triples->addTriple($carol, 'knows', $alice, 'wiki'); 47 | 48 | $this->_triples->addTriple($bob, 'likes', $alice, 'wiki'); 49 | 50 | $this->_triples->addTriple($bob, 'looks like', $img_bob, 'wiki'); 51 | $this->_triples->addTriple($alice, 'looks like', $img_alice, 'wiki'); 52 | $this->_triples->addTriple($carol, 'looks like', $img_carol, 'wiki'); 53 | 54 | $this->_triples->addTriple($bob, 'is rated', 8, 'wiki'); 55 | $this->_triples->addTriple($alice, 'is rated', 10, 'wiki'); 56 | $this->_triples->addTriple($carol, 'is rated', 1, 'wiki'); 57 | 58 | $this->_triples->addTriple($bob, 'has length', '5 ft 10 in', 'wiki'); 59 | $this->_triples->addTriple($alice, 'has length', '5 ft 5 in', 'wiki'); 60 | $this->_triples->addTriple($carol, 'has length', '4 ft 11 in', 'wiki'); 61 | 62 | $this->_triples->addTriple($bob, 'tax rate', '25%', 'wiki'); 63 | $this->_triples->addTriple($alice, 'tax rate', '10%', 'wiki'); 64 | $this->_triples->addTriple($carol, 'tax rate', '2%', 'wiki'); 65 | } 66 | 67 | function assertQueryResult($query, $expectedResult, $message='') { 68 | $relations = $this->_triples->queryRelations($query); 69 | if ($relations === false) { 70 | $this->fail($message.' Query failed.'); 71 | } else { 72 | $this->assertIteratorsEqual($relations, new ArrayIterator($expectedResult), $message); 73 | $relations->closeCursor(); 74 | } 75 | } 76 | 77 | function assertIteratorsEqual($x, $y, $message='') { 78 | $message = $message?$message.': ':''; 79 | do { 80 | $this->assertEquals($x->valid(), $y->valid(), $message.'Number of result and expected rows differ: %s'); 81 | $this->assertEquals($x->current(), $y->current(), $message.'Result row differs from expected one: %s'); 82 | $x->next(); 83 | $y->next(); 84 | } while ($x->valid() || $y->valid()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /_test/stratatest.inc.php: -------------------------------------------------------------------------------- 1 | pluginsEnabled[] = 'strata'; 11 | parent::setUp(); 12 | 13 | // Setup a new database (uncomment the one to use) 14 | //$this->_triples = new helper_plugin_stratastorage_triples(); 15 | $this->_triples = new helper_plugin_strata_triples(); 16 | 17 | // Use SQLite (default) 18 | $this->_triples->_initialize('sqlite::memory:'); 19 | 20 | // Use MySQL, which is set up with: 21 | // CREATE DATABASE strata_test; 22 | // GRANT ALL ON strata_test.* TO ''@localhost; 23 | //$this->_triples->initialize('mysql:dbname=strata_test'); 24 | 25 | // Use PostgreSQL, which is set up with: 26 | // createuser -SDR strata 27 | // createdb -l "en_US.UTF-8" -E UTF8 -T template0 strata_test 28 | //$this->_triples->initialize('pgsql:dbname=strata_test;user=strata'); 29 | 30 | } 31 | 32 | function teardown() { 33 | // Remove the database 34 | $this->_triples->_db->removeDatabase(); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /_test/types.test.php: -------------------------------------------------------------------------------- 1 | _types = new helper_plugin_strata_util(); 13 | } 14 | 15 | function testString() { 16 | $type = $this->_types->loadType('text'); 17 | // Empty hint 18 | $s = $type->normalize('bob', ''); 19 | $this->assertEquals($s, 'bob'); 20 | // Empty hint 21 | $s = $type->normalize('Bob', ''); 22 | $this->assertEquals($s, 'Bob'); 23 | // Numerical hint 24 | $s = $type->normalize('Bob', 10); 25 | $this->assertEquals($s, 'Bob'); 26 | // String hint 27 | $s = $type->normalize('Bob', 'master'); 28 | $this->assertEquals($s, 'Bob'); 29 | // Whitespace 30 | $s = $type->normalize(' Bob ', ''); 31 | $this->assertEquals($s, ' Bob '); 32 | // Special characters 33 | $s = $type->normalize('Bob & Alice', ''); 34 | $this->assertEquals($s, 'Bob & Alice'); 35 | // Unicode 36 | $s = $type->normalize('Één ís één.', ''); 37 | $this->assertEquals($s, 'Één ís één.'); 38 | } 39 | 40 | function testPage() { 41 | $type = $this->_types->loadType('page'); 42 | // Empty hint 43 | $s = $type->normalize('bob', ''); 44 | $this->assertEquals($s, 'bob'); 45 | // Empty hint 46 | $s = $type->normalize('Bob', ''); 47 | $this->assertEquals($s, 'bob'); 48 | // Numerical hint 49 | $s = $type->normalize('Bob', 10); 50 | $this->assertEquals($s, '10:bob'); 51 | // String hint 52 | $s = $type->normalize('Bob', 'master'); 53 | $this->assertEquals($s, 'master:bob'); 54 | // Whitespace 55 | $s = $type->normalize(' Bob ', ''); 56 | $this->assertEquals($s, 'bob'); 57 | // Special characters 58 | $s = $type->normalize('Bob & Alice', ''); 59 | $this->assertEquals($s, 'bob_alice'); 60 | // Unicode 61 | $s = $type->normalize('Één ís één.', ''); 62 | $this->assertEquals($s, 'een_is_een'); 63 | // Relative pathes 64 | $s = $type->normalize('..:.:Bob', 'master:user'); 65 | $this->assertEquals($s, 'master:bob'); 66 | $s = $type->normalize('.:..:Bob', 'master:user'); 67 | $this->assertEquals($s, 'master:bob'); 68 | // Fragments in url (link to namespace start) 69 | $s = $type->normalize(':#Bob', 'master:user'); 70 | $this->assertEquals($s, 'start#bob'); 71 | $s = $type->normalize('.:#Bob', 'master:user'); 72 | $this->assertEquals($s, 'master:user:start#bob'); 73 | $s = $type->normalize('..:#Bob', 'master:user'); 74 | $this->assertEquals($s, 'master:start#bob'); 75 | } 76 | 77 | function testPageWithID() { 78 | // Set ID 79 | global $ID; 80 | $this->assertEquals($ID, null); // Test whether the test suite is initialised as expected. 81 | $ID = 'an_id:sub:current_page'; 82 | 83 | $type = $this->_types->loadType('page'); 84 | // Empty hint 85 | $s = $type->normalize('bob', ''); 86 | $this->assertEquals($s, 'an_id:sub:bob'); 87 | // Empty hint 88 | $s = $type->normalize('Bob', ''); 89 | $this->assertEquals($s, 'an_id:sub:bob'); 90 | // Numerical hint 91 | $s = $type->normalize('Bob', 10); 92 | $this->assertEquals($s, '10:bob'); 93 | // String hint 94 | $s = $type->normalize('Bob', 'master'); 95 | $this->assertEquals($s, 'master:bob'); 96 | // Whitespace 97 | $s = $type->normalize(' Bob ', ''); 98 | $this->assertEquals($s, 'an_id:sub:bob'); 99 | // Special characters 100 | $s = $type->normalize('Bob & Alice', ''); 101 | $this->assertEquals($s, 'an_id:sub:bob_alice'); 102 | // Unicode 103 | $s = $type->normalize('Één ís één.', ''); 104 | $this->assertEquals($s, 'an_id:sub:een_is_een'); 105 | // Relative pathes w.r.t. given namespace 106 | $s = $type->normalize('..:.:Bob', 'master:user'); 107 | $this->assertEquals($s, 'master:bob'); 108 | $s = $type->normalize('.:..:Bob', 'master:user'); 109 | $this->assertEquals($s, 'master:bob'); 110 | // Fragments in url w.r.t. given namespace 111 | $s = $type->normalize('.:#Bob', 'master:user'); 112 | $this->assertEquals($s, 'master:user:start#bob'); 113 | $s = $type->normalize('..:#Bob', 'master:user'); 114 | $this->assertEquals($s, 'master:start#bob'); 115 | $s = $type->normalize('#Bob', 'master:user'); 116 | $this->assertEquals($s, 'an_id:sub:current_page#bob'); 117 | // Relative pathes w.r.t. ID 118 | $s = $type->normalize('..:.:Bob', ''); 119 | $this->assertEquals($s, 'an_id:bob'); 120 | $s = $type->normalize('.:..:Bob', ''); 121 | $this->assertEquals($s, 'an_id:bob'); 122 | // Fragments in url w.r.t. ID 123 | $s = $type->normalize('.:#Bob', ''); 124 | $this->assertEquals($s, 'an_id:sub:start#bob'); 125 | $s = $type->normalize('..:#Bob', ''); 126 | $this->assertEquals($s, 'an_id:start#bob'); 127 | $s = $type->normalize('#Bob', ''); 128 | $this->assertEquals($s, 'an_id:sub:current_page#bob'); 129 | $s = $type->normalize('Other Page#Bob', ''); 130 | $this->assertEquals($s, 'an_id:sub:other_page#bob'); 131 | 132 | // Restore global to avoid interference with other tests 133 | $ID = null; 134 | } 135 | 136 | function testRef() { 137 | $type = $this->_types->loadType('ref'); 138 | // Empty hint 139 | $s = $type->normalize('bob', ''); 140 | $this->assertEquals($s, 'bob'); 141 | // Empty hint 142 | $s = $type->normalize('Bob', ''); 143 | $this->assertEquals($s, 'bob'); 144 | // Numerical hint 145 | $s = $type->normalize('Bob', 10); 146 | $this->assertEquals($s, '10:bob'); 147 | // String hint 148 | $s = $type->normalize('Bob', 'master'); 149 | $this->assertEquals($s, 'master:bob'); 150 | // Whitespace 151 | $s = $type->normalize(' Bob ', ''); 152 | $this->assertEquals($s, 'bob'); 153 | // Special characters 154 | $s = $type->normalize('Bob & Alice', ''); 155 | $this->assertEquals($s, 'bob_alice'); 156 | // Unicode 157 | $s = $type->normalize('Één ís één.', ''); 158 | $this->assertEquals($s, 'een_is_een'); 159 | // Relative pathes 160 | $s = $type->normalize('..:.:Bob', 'master:user'); 161 | $this->assertEquals($s, 'master:bob'); 162 | $s = $type->normalize('.:..:Bob', 'master:user'); 163 | $this->assertEquals($s, 'master:bob'); 164 | // Fragments in url (link to namespace) 165 | $s = $type->normalize('.:#Bob', 'master:user'); 166 | $this->assertEquals($s, 'master:user:start#bob'); 167 | $s = $type->normalize('..:#Bob', 'master:user'); 168 | $this->assertEquals($s, 'master:start#bob'); 169 | $s = $type->normalize(':#Bob', 'master:user'); 170 | $this->assertEquals($s, 'start#bob'); 171 | } 172 | 173 | function testImage() { 174 | $type = $this->_types->loadType('image'); 175 | // Empty hint 176 | $s = $type->normalize('bob.png', ''); 177 | $this->assertEquals($s, 'bob.png'); 178 | // Empty hint 179 | $s = $type->normalize('Bob.png', ''); 180 | $this->assertEquals($s, 'bob.png'); 181 | // Numerical hint 182 | $s = $type->normalize('Bob.png', 10); 183 | $this->assertEquals($s, 'bob.png'); 184 | // Whitespace 185 | $s = $type->normalize(' Bob.png ', ''); 186 | $this->assertEquals($s, 'bob.png'); 187 | // Special characters 188 | $s = $type->normalize('Bob & Alice.png', ''); 189 | $this->assertEquals($s, 'bob_alice.png'); 190 | // Unicode 191 | $s = $type->normalize('Één ís één.png', ''); 192 | $this->assertEquals($s, 'een_is_een.png'); 193 | } 194 | } 195 | 196 | -------------------------------------------------------------------------------- /action.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | /** 13 | * This action component exists to allow the definition of 14 | * the type autoloader. 15 | */ 16 | class action_plugin_strata extends DokuWiki_Action_Plugin { 17 | 18 | /** 19 | * Register function called by DokuWiki to allow us 20 | * to register events we're interested in. 21 | * 22 | * @param controller object the controller to register with 23 | */ 24 | public function register(Doku_Event_Handler $controller) { 25 | $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, '_io_page_write'); 26 | $controller->register_hook('PARSER_METADATA_RENDER', 'BEFORE', $this, '_parser_metadata_render_before'); 27 | $controller->register_hook('STRATA_PREVIEW_METADATA_RENDER', 'BEFORE', $this, '_parser_metadata_render_before'); 28 | $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, '_preview_before'); 29 | $controller->register_hook('TPL_ACT_RENDER', 'AFTER', $this, '_preview_after'); 30 | 31 | $controller->register_hook('PARSER_METADATA_RENDER', 'AFTER', $this, '_parser_metadata_render_after'); 32 | $controller->register_hook('STRATA_PREVIEW_METADATA_RENDER', 'AFTER', $this, '_parser_metadata_render_after'); 33 | } 34 | 35 | 36 | /** 37 | * Triggers before preview xhtml render, 38 | * allows plugins to metadata render on the preview. 39 | */ 40 | public function _preview_before(&$event, $param) { 41 | global $ACT; 42 | global $TEXT; 43 | global $SUF; 44 | global $PRE; 45 | global $ID; 46 | global $METADATA_RENDERERS; 47 | 48 | if($ACT == 'preview') { 49 | $triples =& plugin_load('helper', 'strata_triples'); 50 | $triples->beginPreview(); 51 | 52 | $text = $PRE.$TEXT.$SUF; 53 | $orig = p_read_metadata($ID); 54 | 55 | // store the original metadata in the global $METADATA_RENDERERS so p_set_metadata can use it 56 | $METADATA_RENDERERS[$ID] =& $orig; 57 | 58 | // add an extra key for the event - to tell event handlers the page whose metadata this is 59 | $orig['page'] = $ID; 60 | $evt = new Doku_Event('STRATA_PREVIEW_METADATA_RENDER', $orig); 61 | if ($evt->advise_before()) { 62 | // get instructions 63 | $instructions = p_get_instructions($text); 64 | if(is_null($instructions)){ 65 | unset($METADATA_RENDERERS[$ID]); 66 | return null; // something went wrong with the instructions 67 | } 68 | 69 | // set up the renderer 70 | $renderer = new renderer_plugin_strata(); 71 | $renderer->meta =& $orig['current']; 72 | $renderer->persistent =& $orig['persistent']; 73 | 74 | // loop through the instructions 75 | foreach ($instructions as $instruction){ 76 | // execute the callback against the renderer 77 | call_user_func_array(array(&$renderer, $instruction[0]), (array) $instruction[1]); 78 | } 79 | 80 | $evt->result = array('current'=>&$renderer->meta,'persistent'=>&$renderer->persistent); 81 | } 82 | $evt->advise_after(); 83 | 84 | // clean up 85 | unset($METADATA_RENDERERS[$id]); 86 | } 87 | } 88 | 89 | 90 | public function _preview_after(&$event, $param) { 91 | global $ACT; 92 | 93 | if($ACT == 'preview') { 94 | $triples =& plugin_load('helper', 'strata_triples'); 95 | $triples->endPreview(); 96 | } 97 | } 98 | 99 | /** 100 | * Triggered whenever a page is written. We need to handle 101 | * this event because metadata is not rendered if a page is removed. 102 | */ 103 | public function _io_page_write(&$event, $param) { 104 | // only remove triples if page is a new revision, or if it is removed 105 | if($event->data[3] == false || $event->data[0][1] == '') { 106 | $id = ltrim($event->data[1].':'.$event->data[2],':'); 107 | $this->_purge_data($id); 108 | } 109 | } 110 | 111 | /** 112 | * Triggered before metadata is going to be rendered. We 113 | * remove triples previously generated by the page that is going to 114 | * be rendered so we don't get duplicate entries. 115 | */ 116 | public function _parser_metadata_render_before(&$event, $param) { 117 | $this->_purge_data($event->data['page']); 118 | } 119 | 120 | /** 121 | * Triggered after metadata has been rendered. 122 | * We check the fixTitle flag, and if it is present, we 123 | * add the entry title. 124 | */ 125 | public function _parser_metadata_render_after(&$event, $param) { 126 | $id = $event->data['page']; 127 | 128 | $current =& $event->data['current']; 129 | 130 | if(isset($current['strata']['fixTitle']) && $current['strata']['fixTitle']) { 131 | // get helpers 132 | $triples =& plugin_load('helper', 'strata_triples'); 133 | $util =& plugin_load('helper', 'strata_util'); 134 | 135 | $title = $current['title']; 136 | if(!$title) { 137 | $title = noNS($id); 138 | } 139 | 140 | $title = $util->loadType('text')->normalize($title,''); 141 | 142 | $triples->addTriple($id, $util->getTitleKey(), $title, $id); 143 | } 144 | } 145 | 146 | /** 147 | * Purges the data for a single page id. 148 | * 149 | * @param id string the page that needs to be purged 150 | */ 151 | private function _purge_data($id) { 152 | // get triples helper 153 | $triples =& plugin_load('helper', 'strata_triples'); 154 | 155 | // remove all triples defined in this graph 156 | $triples->removeTriples(null,null,null,$id); 157 | } 158 | } 159 | 160 | /** 161 | * Strata 'pluggable' autoloader. This function is responsible 162 | * for autoloading classes that should be pluggable by external 163 | * plugins. 164 | * 165 | * @param fullname string the name of the class to load 166 | */ 167 | function plugin_strata_autoload($fullname) { 168 | static $classes = null; 169 | if(is_null($classes)) $classes = array( 170 | 'strata_exception' => DOKU_PLUGIN.'strata/lib/strata_exception.php', 171 | 'strata_querytree_visitor' => DOKU_PLUGIN.'strata/lib/strata_querytree_visitor.php', 172 | 'plugin_strata_type' => DOKU_PLUGIN.'strata/lib/strata_type.php', 173 | 'plugin_strata_aggregate' => DOKU_PLUGIN.'strata/lib/strata_aggregate.php', 174 | ); 175 | 176 | if(isset($classes[$fullname])) { 177 | require_once($classes[$fullname]); 178 | return; 179 | } 180 | 181 | // only load matching components 182 | if(preg_match('/^plugin_strata_(type|aggregate)_(.*)$/',$fullname, $matches)) { 183 | // use descriptive names 184 | list(,$kind,$name) = $matches; 185 | 186 | // glob to find the required file 187 | $filenames = glob(DOKU_PLUGIN."*/{$kind}s/{$name}.php"); 188 | if($filenames === false || count($filenames) == 0) { 189 | // if we have no file, fake an implementation 190 | eval("class $fullname extends plugin_strata_{$kind} { };"); 191 | } else { 192 | // include the file 193 | require_once $filenames[0]; 194 | // If the class still does not exist, the required file does not define the class, so we fall back 195 | // to the default 196 | if(!class_exists($fullname)) { 197 | eval("class $fullname extends plugin_strata_{$kind} { };"); 198 | } 199 | } 200 | 201 | return; 202 | } 203 | } 204 | 205 | // register autoloader with SPL loader stack 206 | spl_autoload_register('plugin_strata_autoload'); 207 | -------------------------------------------------------------------------------- /aggregates/count.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The counting aggregator. 11 | */ 12 | class plugin_strata_aggregate_count extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | return array(count($values)); 15 | } 16 | 17 | function getInfo() { 18 | return array( 19 | 'desc'=>'Counts the number of items.', 20 | 'tags'=>array() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /aggregates/first.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The first aggregator. 11 | */ 12 | class plugin_strata_aggregate_first extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | $val = reset($values); 15 | if($val === false ) { 16 | return array(); 17 | } 18 | 19 | return array($val); 20 | } 21 | 22 | function getInfo() { 23 | return array( 24 | 'desc'=>'Selects only the first element.', 25 | 'tags'=>array() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /aggregates/last.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The last aggregator. 11 | */ 12 | class plugin_strata_aggregate_last extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | $val = end($values); 15 | if($val === false ) { 16 | return array(); 17 | } 18 | 19 | return array($val); 20 | } 21 | 22 | function getInfo() { 23 | return array( 24 | 'desc'=>'Selects only the last element.', 25 | 'tags'=>array() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /aggregates/max.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The maximum aggregator. 11 | */ 12 | class plugin_strata_aggregate_max extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | if($hint == 'strict') $values = array_filter($values, 'is_numeric'); 15 | if(empty($values)) return array(); 16 | return array(max($values)); 17 | } 18 | 19 | function getInfo() { 20 | return array( 21 | 'desc'=>'Returns the maximum value. Any item that does not have a clear numeric value (i.e. starts with a number) is counted as 0. If the \'strict\' hint is used, values that are not strictly numeric (i.e. contains only a number) are ignored.', 22 | 'hint'=>'\'strict\' to ignore non-numeric values', 23 | 'tags'=>array('numeric') 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /aggregates/min.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The minimum aggregator. 11 | */ 12 | class plugin_strata_aggregate_min extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | if($hint == 'strict') $values = array_filter($values, 'is_numeric'); 15 | if(empty($values)) return array(); 16 | return array(min($values)); 17 | } 18 | 19 | function getInfo() { 20 | return array( 21 | 'desc'=>'Returns the minimum value. Any item that does not have a clear numeric value (i.e. starts with a number) is counted as 0. If the \'strict\' hint is used, values that are not strictly numeric (i.e. contains only a number) are ignored.', 22 | 'hint'=>'\'strict\' to ignore non-numeric values', 23 | 'tags'=>array('numeric') 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /aggregates/sum.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The summation aggregator. 11 | */ 12 | class plugin_strata_aggregate_sum extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | if($hint == 'strict') { 15 | return array_reduce($values, function(&$state, $item) { 16 | if(is_numeric($item)) { 17 | $state[0] += $item; 18 | } else { 19 | $state[] = $item; 20 | } 21 | return $state; 22 | }, array(0)); 23 | } else { 24 | return array(array_sum($values)); 25 | } 26 | } 27 | 28 | function getInfo() { 29 | return array( 30 | 'desc'=>'Sums up all items. Any item that does not have a clear numeric value (i.e. starts with a number) is counted as 0. If the \'strict\' hint is used, values that are not strictly numeric (i.e. contains only a number) are left intact.', 31 | 'hint'=>'\'strict\' to leave non-numeric values', 32 | 'tags'=>array('numeric') 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /aggregates/unique.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The unique aggregator. 11 | */ 12 | class plugin_strata_aggregate_unique extends plugin_strata_aggregate { 13 | function aggregate($values, $hint = null) { 14 | return array_unique($values); 15 | } 16 | 17 | function getInfo() { 18 | return array( 19 | 'desc'=>'Removes all duplicates.', 20 | 'tags'=>array() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /conf/default.php: -------------------------------------------------------------------------------- 1 | '/^\[([a-z0-9]+)(?:::([^\)]*))?\]$/'); 4 | $meta['predicate_type'] = array('string', '_pattern'=>'/^\[([a-z0-9]+)(?:::([^\)]*))?\]$/'); 5 | $meta['default_dsn'] = array('string', '_pattern'=>'/.+:.*/'); 6 | $meta['isa_key'] = array('string'); 7 | $meta['title_key'] = array('string'); 8 | $meta['debug'] = array('onoff'); 9 | 10 | $meta['enable_entry'] = array('onoff'); 11 | -------------------------------------------------------------------------------- /driver/driver.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | // Define the location of the local credentials file. 10 | if(!defined('STRATA_CREDENTIALS')) define('STRATA_CREDENTIALS', DOKU_PLUGIN.'strata/credentials.local.php'); 11 | 12 | /** 13 | * The base class for database drivers. 14 | */ 15 | abstract class plugin_strata_driver { 16 | 17 | /** 18 | * Whether the driver should generate debug output. 19 | */ 20 | var $_debug; 21 | 22 | /** 23 | * The dsn. 24 | */ 25 | var $_dsn; 26 | 27 | /** 28 | * The PDO database object. 29 | */ 30 | var $_db; 31 | 32 | /** 33 | * Create a new database driver. 34 | * 35 | * @param debug boolean whether the created driver should generate debug output. 36 | */ 37 | function __construct($debug=false) { 38 | $this->_debug = $debug; 39 | $this->util =& plugin_load('helper', 'strata_util'); 40 | } 41 | 42 | /** 43 | * Produces the syntax to cast something to a number. 44 | * 45 | * @param val string the thing to cast 46 | */ 47 | public function castToNumber($val) { 48 | return "CAST($val AS NUMERIC)"; 49 | } 50 | 51 | /** 52 | * Casts the given value to a case insensitive variant. 53 | * 54 | * This cast can, for example, be using a case insensitive collation or using the function 'lower' (default). 55 | * @param val string the thing make case insensitive 56 | */ 57 | public function ci($val='?') { 58 | return "lower($val)"; 59 | } 60 | 61 | /** 62 | * Returns the syntax for case-insensitive string comparison. 63 | * 64 | * Preferably, this syntax should allow % as a wildcard (e.g. as done by LIKE). 65 | */ 66 | public function stringCompare() { 67 | return 'LIKE'; 68 | } 69 | 70 | /** 71 | * Returns the terms on which we should order. 72 | * 73 | * Ideally, the ordering should be natural, that is '2 apples' is sorted before '10 pears'. 74 | * However, depending on the supported database, ordering can vary between string and natural ordering, including any compromises. 75 | * @param val string the thing to sort on 76 | * @return an array of terms to sort on 77 | */ 78 | public function orderBy($val) { 79 | return array( 80 | $this->castToNumber($val), 81 | $this->ci($val) 82 | ); 83 | } 84 | 85 | /** 86 | * Open the database connection. 87 | * 88 | * @param dsn string the dsn to use for connecting 89 | * @return boolean true when connecting was successful 90 | */ 91 | public function connect($dsn) { 92 | $this->_dsn = $dsn; 93 | try { 94 | $this->_db = $this->initializePDO($dsn); 95 | } catch(PDOException $e) { 96 | if ($this->_debug) { 97 | msg(sprintf($this->util->getLang('driver_failed_detail'), hsc($dsn), hsc($e->getMessage())), -1); 98 | } else { 99 | msg($this->util->getLang('driver_failed'), -1); 100 | } 101 | return false; 102 | } 103 | $this->initializeConnection(); 104 | return true; 105 | } 106 | 107 | /** 108 | * Initialize the PDO object. 109 | * 110 | * @param dsn string the dsn to use for construction 111 | * @return the PDO object 112 | */ 113 | protected function initializePDO($dsn) { 114 | $credentials = array(null,null); 115 | if(@file_exists(STRATA_CREDENTIALS)) { 116 | $credentials = include(STRATA_CREDENTIALS); 117 | } 118 | return new PDO($dsn, $credentials[0], $credentials[1]); 119 | } 120 | 121 | 122 | /** 123 | * Initialises a connection directly after the connection was made (e.g. by setting the character set of the connection). 124 | */ 125 | protected function initializeConnection() {} 126 | 127 | /** 128 | * Determines whether the database is initialised. 129 | * 130 | * @return boolean true if the database is initialised 131 | */ 132 | public abstract function isInitialized(); 133 | 134 | /** 135 | * Initialises the database by setting up all tables. 136 | * 137 | * This implementation looks for a file called 'setup-@driver@.sql' and executes all SQL statements in that file. 138 | * Here, '@driver@' represents the database driver, such as 'sqlite'. 139 | * 140 | * @return boolean true if the database was initialised successfully 141 | */ 142 | public function initializeDatabase() { 143 | if($this->_db == false) return false; 144 | 145 | // determine driver 146 | list($driver, $connection) = explode(':', $this->_dsn, 2); 147 | if ($this->_debug) msg(sprintf($this->util->getLang('driver_setup_start'), hsc($driver))); 148 | 149 | // load SQL script 150 | $sqlfile = DOKU_PLUGIN . "strata/sql/setup-$driver.sql"; 151 | 152 | $sql = io_readFile($sqlfile, false); 153 | $lines = explode("\n",$sql); 154 | 155 | // remove empty lines and comment lines 156 | // (this makes sure that a semicolon in the comment doesn't break the script) 157 | $sql = ''; 158 | foreach($lines as $line) { 159 | $line = preg_replace('/--.*$/','',$line); 160 | if(trim($line," \t\n\r") == '') continue; 161 | $sql .= $line; 162 | } 163 | 164 | // split the script into distinct statements 165 | $sql = explode(';', $sql); 166 | 167 | // execute the database initialisation script in a transaction 168 | // (doesn't work in all databases, but provides some failsafe where it works) 169 | $this->beginTransaction(); 170 | foreach($sql as $s) { 171 | // skip empty lines (usually the last line is empty, due to the final semicolon) 172 | if(trim($s) == '') continue; 173 | 174 | if ($this->_debug) msg(sprintf($this->util->getLang('driver_setup_statement'),hsc($s))); 175 | if(!$this->query($s, $this->util->getLang('driver_setup_failed'))) { 176 | $this->rollBack(); 177 | return false; 178 | } 179 | } 180 | $this->commit(); 181 | 182 | if($this->_debug) msg($this->util->getLang('driver_setup_succes'), 1); 183 | 184 | return true; 185 | } 186 | 187 | /** 188 | * Removes a database that was initialized before. 189 | * 190 | * @return whether the database was removed successfully 191 | */ 192 | public function removeDatabase() { 193 | return $this->query('DROP TABLE data', $this->util->getLang('driver_remove_failed')); 194 | } 195 | 196 | /** 197 | * Prepares a query and reports any problems to Dokuwiki. 198 | * 199 | * @param query string the query to prepare 200 | * @return the prepared statement 201 | */ 202 | public function prepare($query) { 203 | if($this->_db == false) return false; 204 | 205 | $result = $this->_db->prepare($query); 206 | if ($result === false) { 207 | $error = $this->_db->errorInfo(); 208 | msg(sprintf($this->util->getLang('driver_prepare_failed'),hsc($query), hsc($error[2])),-1); 209 | return false; 210 | } 211 | 212 | return $result; 213 | } 214 | 215 | /** 216 | * Executes a query and reports any problems to Dokuwiki. 217 | * 218 | * @param query string the query to execute 219 | * @param message string message to report when executing the query fails 220 | * @return whether querying succeeded 221 | */ 222 | public function query($query, $message=false) { 223 | if($this->_db == false) return false; 224 | 225 | if($message === false) { 226 | $message = $this->util->getLang('driver_query_failed_default'); 227 | } 228 | 229 | $res = $this->_db->query($query); 230 | if ($res === false) { 231 | $error = $this->_db->errorInfo(); 232 | msg(sprintf($this->utiutil->getLang('driver_query_failed'), $message, hsc($query), hsc($error[2])),-1); 233 | return false; 234 | } 235 | return true; 236 | } 237 | 238 | private $transactions = array(); 239 | private $transactionCount = 0; 240 | 241 | private function _transactionId() { 242 | return "t".$this->transactionCount++; 243 | } 244 | 245 | /** 246 | * Begins a transaction. 247 | */ 248 | public function beginTransaction() { 249 | if($this->_db == false) return false; 250 | 251 | if(count($this->transactions)) { 252 | $t = $this->_transactionId(); 253 | array_push($this->transactions, $t); 254 | $this->_db->query('SAVEPOINT '.$t.';'); 255 | return true; 256 | } else { 257 | array_push($this->transactions, 'work'); 258 | return $this->_db->beginTransaction(); 259 | } 260 | } 261 | 262 | /** 263 | * Commits the current transaction. 264 | */ 265 | public function commit() { 266 | if($this->_db == false) return false; 267 | 268 | array_pop($this->transactions); 269 | if(count($this->transactions)) { 270 | return true; 271 | } else { 272 | return $this->_db->commit(); 273 | } 274 | } 275 | 276 | /** 277 | * Rolls back the current transaction. 278 | */ 279 | public function rollBack() { 280 | if($this->_db == false) return false; 281 | 282 | $t = array_pop($this->transactions); 283 | if(count($this->transactions)) { 284 | $this->_db->query('ROLLBACK TO '.$t.';'); 285 | return true; 286 | } else { 287 | return $this->_db->rollBack(); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /driver/mysql.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | require_once(DOKU_PLUGIN.'strata/driver/driver.php'); 10 | 11 | /** 12 | * The MySQL database driver. 13 | */ 14 | class plugin_strata_driver_mysql extends plugin_strata_driver { 15 | 16 | public function castToNumber($val) { 17 | return "CAST($val AS DECIMAL)"; 18 | } 19 | 20 | public function ci($val='?') { 21 | return "$val COLLATE utf8mb4_unicode_ci"; 22 | } 23 | 24 | protected function initializeConnection() { 25 | $this->query('SET NAMES utf8mb4'); 26 | $this->query("SET sql_mode = 'PIPES_AS_CONCAT'"); // Ensure that || works as string concatenation 27 | } 28 | 29 | public function isInitialized() { 30 | return $this->_db->query("SHOW TABLES LIKE 'data'")->rowCount() != 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /driver/pgsql.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | require_once(DOKU_PLUGIN.'strata/driver/driver.php'); 10 | 11 | /** 12 | * The MySQL database driver. 13 | */ 14 | class plugin_strata_driver_pgsql extends plugin_strata_driver { 15 | 16 | public function stringCompare() { 17 | return 'ILIKE'; 18 | } 19 | 20 | public function castToNumber($val) { 21 | return "SUBSTRING($val FROM E'^(-?[0-9]+\\\\.?[0-9]*)')::numeric"; 22 | } 23 | 24 | public function orderBy($val) { 25 | return array( 26 | "$val IS NOT NULL", 27 | $this->castToNumber($val), 28 | $val 29 | ); 30 | } 31 | 32 | public function isInitialized() { 33 | return $this->_db->query("SELECT * FROM pg_tables WHERE schemaname = 'public' AND tablename = 'data'")->rowCount() != 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /driver/sqlite.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | require_once(DOKU_PLUGIN.'strata/driver/driver.php'); 10 | 11 | /** 12 | * The base class for database drivers. 13 | */ 14 | class plugin_strata_driver_sqlite extends plugin_strata_driver { 15 | public function ci($val='?') { 16 | return "$val COLLATE NOCASE"; 17 | } 18 | 19 | public function isInitialized() { 20 | return $this->_db->query("SELECT count(*) FROM sqlite_master WHERE name = 'data'")->fetchColumn() != 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /helper/util.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | /** 13 | * This utility helper offers methods for configuration handling 14 | * type and aggregator loading, and rendering. 15 | */ 16 | class helper_plugin_strata_util extends DokuWiki_Plugin { 17 | /** 18 | * Constructor. 19 | */ 20 | function __construct() { 21 | // we can't depend on the syntax helper due to recursive dependencies. 22 | // Since we really only need the pattern helper anyway, we grab it 23 | // directly. (This isn't the nicest solution -- but depending on 24 | // a helper that depends on us isn't either) 25 | $this->patterns = helper_plugin_strata_syntax::$patterns; 26 | } 27 | 28 | function getMethods() { 29 | $result = array(); 30 | return $result; 31 | } 32 | 33 | /** 34 | * The loaded types and aggregates cache. 35 | */ 36 | var $loaded = array(); 37 | 38 | /** 39 | * Loads something. 40 | */ 41 | private function _load($kind, $name, $default) { 42 | // handle null value 43 | if($name == null) { 44 | $name = $default; 45 | } 46 | 47 | // use cache if possible 48 | if(empty($this->loaded[$kind][$name])) { 49 | $class = "plugin_strata_${kind}_${name}"; 50 | $this->loaded[$kind][$name] = new $class(); 51 | } 52 | 53 | return $this->loaded[$kind][$name]; 54 | 55 | } 56 | 57 | /** 58 | * Loads a type. 59 | */ 60 | function loadType($type) { 61 | list($default,) = $this->getDefaultType(); 62 | return $this->_load('type', $type, $default); 63 | } 64 | 65 | /** 66 | * Loads an aggregate. 67 | */ 68 | function loadAggregate($aggregate) { 69 | return $this->_load('aggregate', $aggregate, 'all'); 70 | } 71 | 72 | /** 73 | * Parses a 'name(hint)' pattern. 74 | * 75 | * @param string string the text to parse 76 | * @return an array with a name and hint, or false 77 | */ 78 | function parseType($string) { 79 | $p = $this->patterns; 80 | if(preg_match("/^({$p->type})?$/", $string, $match)) { 81 | list($type, $hint) = $p->type($match[1]); 82 | return array($type, $hint); 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | /** 89 | * The parsed configuration types. 90 | */ 91 | var $configTypes = array(); 92 | 93 | /** 94 | * Parses a type from configuration. 95 | */ 96 | function _parseConfigType($key) { 97 | // lazy parse 98 | if(empty($this->configTypes[$key])) { 99 | // parse 100 | $this->configTypes[$key] = $this->parseType($this->getConf($key)); 101 | 102 | // handle failed parse 103 | if($this->configTypes[$key] === false) { 104 | msg(sprintf($this->getLang('error_types_config'), $key), -1); 105 | $this->configTypes[$key] = array( 106 | 'text', 107 | null 108 | ); 109 | } 110 | } 111 | 112 | return $this->configTypes[$key]; 113 | } 114 | 115 | /** 116 | * Returns the default type. 117 | */ 118 | function getDefaultType() { 119 | return $this->_parseConfigType('default_type'); 120 | } 121 | 122 | /** 123 | * Returns the type used for predicates. 124 | */ 125 | function getPredicateType() { 126 | return $this->_parseConfigType('predicate_type'); 127 | } 128 | 129 | /** 130 | * Returns the normalized value for the 'is a' predicate. 131 | */ 132 | function getIsaKey($normalized=true) { 133 | $result = $this->getConf('isa_key'); 134 | if($normalized) $result = $this->normalizePredicate($result); 135 | return $result; 136 | } 137 | 138 | /** 139 | * Returns the normalized valued for the 'title' predicate. 140 | */ 141 | function getTitleKey($normalized=true) { 142 | $result = $this->getConf('title_key'); 143 | if($normalized) $result = $this->normalizePredicate($result); 144 | return $result; 145 | } 146 | 147 | /** 148 | * Normalizes a predicate. 149 | * 150 | * @param p the string to normalize 151 | */ 152 | function normalizePredicate($p) { 153 | list($type, $hint) = $this->getPredicateType(); 154 | return $this->loadType($type)->normalize($p, $hint); 155 | } 156 | 157 | /** 158 | * Renders a predicate as a full field. 159 | * 160 | * @param mode the rendering mode 161 | * @param R the renderer 162 | * @param T the triples helper 163 | * @param p the predicate 164 | */ 165 | function renderPredicate($mode, &$R, &$T, $p) { 166 | list($typename, $hint) = $this->getPredicateType(); 167 | $this->renderField($mode, $R, $T, $p, $typename, $hint); 168 | } 169 | 170 | /** 171 | * Renders a single value. If the mode is xhtml, this also surrounds the value with 172 | * the necessary tag to allow styling of types and to ease extraction of values 173 | * with javascript. 174 | * 175 | * @param mode the rendering mode 176 | * @param R the renderer 177 | * @param T the triples helper 178 | * @param value the value to render 179 | * @param typename name of the type 180 | * @param hint optional type hint 181 | * @param type optional type object, if omitted the typename will be used to get the type 182 | */ 183 | function renderValue($mode, &$R, &$T, $value, $typename, $hint=null, &$type=null) { 184 | // load type if needed 185 | if($type == null) $type = $this->loadType($typename); 186 | 187 | // render value 188 | $this->openValue($mode, $R, $typename); 189 | $type->render($mode, $R, $T, $value, $hint); 190 | $this->closeValue($mode, $R); 191 | } 192 | 193 | /** 194 | * Renders multiple values. If the mode is xhtml, this also surrounds the field with 195 | * the necessary tag to allow styling of fields and to ease extraction of values 196 | * with javascript. 197 | * 198 | * @param mode the rendering mode 199 | * @param R the renderer 200 | * @param T the triples helper 201 | * @param values a list of values to render, or optionally a single value 202 | * @param typename the name of the type 203 | * @param hint optional type hint 204 | * @param type optional type object, if omitted typename will be used 205 | * @param field the field name of this field 206 | * @param separator the seperation string to use in-between values 207 | */ 208 | function renderField($mode, &$R, &$T, $values, $typename, $hint=null, &$type=null, $field=null, $separator=', ') { 209 | // arrayfication of values (if a single value is given) 210 | if(!is_array($values)) $values = array($values); 211 | 212 | // load type if needed 213 | if($type == null) $type = $this->loadType($typename); 214 | 215 | // render values 216 | $firstValue = true; 217 | $this->openField($mode, $R, $field); 218 | foreach($values as $value) { 219 | if(!$firstValue) $R->cdata($separator); 220 | $this->renderValue($mode, $R, $T, $value, $typename, $hint, $type); 221 | $firstValue = false; 222 | } 223 | $this->closeField($mode, $R); 224 | } 225 | 226 | function openField($mode, &$R, $field=null) { 227 | if($mode == 'xhtml') $R->doc .= ''; 228 | } 229 | 230 | function closeField($mode, &$R) { 231 | if($mode == 'xhtml') $R->doc .= ''; 232 | } 233 | 234 | function openValue($mode, &$R, $typename) { 235 | if($mode == 'xhtml') $R->doc .= ''; 236 | } 237 | 238 | function closeValue($mode, &$R) { 239 | if($mode == 'xhtml') $R->doc .= ''; 240 | } 241 | 242 | function renderCaptions($mode, &$R, $fields) { 243 | if($mode == 'xhtml') { 244 | foreach($fields as $f) { 245 | $R->doc .= ''.DOKU_LF; 248 | } 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwanders/dokuwiki-strata/23c4c421c9b8d7033f56cce50bc77abf3e92f938/images/sort_asc.png -------------------------------------------------------------------------------- /images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwanders/dokuwiki-strata/23c4c421c9b8d7033f56cce50bc77abf3e92f938/images/sort_both.png -------------------------------------------------------------------------------- /images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwanders/dokuwiki-strata/23c4c421c9b8d7033f56cce50bc77abf3e92f938/images/sort_desc.png -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | text'; 4 | 5 | $lang['error_triples_nodriver'] = 'Strata storage: no complementary driver for PDO driver %s.'; 6 | $lang['error_triples_remove'] = 'Strata storage: Failed to remove triples: %s'; 7 | $lang['error_triples_fetch'] = 'Strata storage: Failed to fetch triples: %s'; 8 | $lang['error_triples_add'] = 'Strata storage: Failed to add triples: %s'; 9 | $lang['error_triples_query'] = 'Strata storage: Failed to execute query: %s'; 10 | $lang['error_triples_node'] = 'Strata storage: Unknown abstract query tree node type \'%s\''; 11 | 12 | $lang['debug_sql'] = 'Debug SQL: %s'; 13 | $lang['debug_literals'] = 'Debug Literals:
%s
'; 14 | 15 | $lang['driver_failed_detail'] = 'Strata storage: Failed to open data source \'%s\': %s'; 16 | $lang['driver_failed'] = 'Strata storage: Failed to open data source.'; 17 | $lang['driver_setup_start'] = 'Strata storage: Setting up %s database.'; 18 | $lang['driver_setup_statement'] = 'Strata storage: Executing \'%s\'.'; 19 | $lang['driver_setup_failed'] = 'Failed to set up database'; 20 | $lang['driver_setup_succes'] = 'Strata storage: Database set up successful!'; 21 | $lang['driver_remove_failed'] = 'Failed to remove database'; 22 | $lang['driver_prepare_failed'] = 'Strata storage: Failed to prepare query \'%s\': %s'; 23 | $lang['driver_query_failed'] = 'Strata storage: %s (with \'%s\'): %s'; 24 | $lang['driver_query_failed_default'] = 'Query failed'; 25 | 26 | $lang['unnamed_group'] = 'unnamed block'; 27 | $lang['named_group'] = '\'%s\' block'; 28 | 29 | $lang['error_entry_block'] = 'I don\'t know what to do with the %s in the \'%s\' data entry'; 30 | $lang['error_entry_line'] = 'I don\'t understand data entry line \'%s\''; 31 | 32 | $lang['error_pattern_garbage'] = 'I don\'t know what to do with the text after the object variable.'; 33 | 34 | $lang['error_query_bothfields'] = 'Query contains both fields block and normal selection'; 35 | $lang['error_query_fieldsgroups'] = 'I don\'t know how to handle a query containing multiple fields block.'; 36 | $lang['error_query_fieldsblock'] = 'I don\'t know what to do with the %s in the fields block.'; 37 | $lang['error_query_noselect'] = 'I don\'t know which fields to select.'; 38 | $lang['error_query_unknownselect'] = 'Query selects unknown field \'%s\'.'; 39 | 40 | $lang['error_query_outofwhere'] = 'I don\'t know what to do with things outside of the where block.'; 41 | $lang['error_query_singlewhere'] = 'A query should contain at most a single where block.'; 42 | 43 | $lang['error_query_multisort'] = 'I don\'t know what to do with multiple sort blocks.'; 44 | $lang['error_query_sortblock'] = 'I can\'t handle blocks in a sort block.'; 45 | $lang['error_query_sortvar'] = 'sort block uses out-of-scope variable \'%s\'.'; 46 | $lang['error_query_sortline'] = 'I can\'t handle line \'%s\' in the sort block.'; 47 | $lang['error_query_selectvar'] = 'selected variable \'%s\' is out-of-scope.'; 48 | $lang['error_query_group'] = 'Unexpected %s in query.'; 49 | $lang['error_query_unionblocks'] = 'Lines or named blocks inside a union block. I can only handle unnamed blocks inside a union block.'; 50 | $lang['error_query_unionreq'] = 'I need at least 2 unnamed blocks inside a union block.'; 51 | $lang['error_query_pattern'] = 'Unknown triple pattern or filter pattern \'%s\'.'; 52 | $lang['error_query_fieldsline'] = 'Weird line \'%s\' in fields block.'; 53 | $lang['error_syntax_braces'] = 'Unmatched braces in %s'; 54 | 55 | $lang['error_query_multigrouping'] = 'I don\'t know what to do with multiple group blocks.'; 56 | $lang['error_query_groupblock'] = 'I can\'t handle blocks in a group block.'; 57 | $lang['error_query_groupvar'] = 'group block uses out-of-scope variable \'%s\'.'; 58 | $lang['error_query_groupline'] = 'I can\'t handle line \'%s\' in the group blocks.'; 59 | $lang['error_query_groupeverything'] = 'I can\'t group everything if other variables are mentioned.'; 60 | 61 | $lang['error_query_multiconsidering'] = 'I don\'t know what to do with multiple consider blocks.'; 62 | $lang['error_query_considerblock'] = 'I can\'t handle considers in a consider block.'; 63 | $lang['error_query_considervar'] = 'consider block uses out-of-scope variable \'%s\'.'; 64 | $lang['error_query_considerline'] = 'I can\'t handle line \'%s\' in the consider block.'; 65 | 66 | $lang['error_query_grouppattern'] = 'I can\'t handle a block without at least one triple pattern or union block.'; 67 | 68 | $lang['error_query_filterscope'] = 'Filter uses out-of-scope variable \'%s\'.'; 69 | 70 | $lang['content_error_explanation'] = 'An error ocurred'; 71 | $lang['content_error_explanation__non_xhtml'] = 'An error ocurred (see online wikipage for details)'; 72 | 73 | $lang['data_entry_previous'] = '← Previous'; 74 | $lang['data_entry_next'] = 'Next →'; 75 | 76 | // UI group 77 | $lang['error_property_weirdgroupline'] = '%s block contains weird line \'%s\', use: property: value.'; 78 | $lang['error_property_unknowngroup'] = '%s block cannot handle column \'%s\'.'; 79 | $lang['error_property_unknownproperty'] = '%s block does not know property \'%s\', only %s are known.'; 80 | $lang['error_property_multi'] = '%s block accepts property \'%s\' only once.'; 81 | $lang['error_property_notmulti'] = '%s property \'%s\' expects at least %d values, but only one is given. Try using \'%s*: first value, second value\'.'; 82 | $lang['error_property_occur'] = '%s property \'%s\' expects %d values instead of the given %d.'; 83 | $lang['error_property_occurrange'] = '%s property \'%s\' expects %d to %d values instead of the given %d.'; 84 | $lang['error_property_invalidchoice'] = '%s property \'%s\' cannot have value \'%s\', it only accepts %s.'; 85 | $lang['error_property_patterndesc'] = '%s property \'%s\' cannot have value \'%s\', it only accepts %s.'; 86 | $lang['error_property_pattern'] = '%s property \'%s\' cannot have value \'%s\', it only accepts values matching %s.'; 87 | 88 | $lang['property_title_values'] = 'Possible values: %s'; 89 | $lang['property_title_synonyms'] = 'Synonyms: %s'; 90 | -------------------------------------------------------------------------------- /lang/en/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['default_type'] = 'デフォルトの値タイプ'; 9 | $lang['predicate_type'] = 'デフォルト predicate タイプ'; 10 | $lang['default_dsn'] = 'デフォルトデータベースソース名(PDO が使用する)'; 11 | $lang['isa_key'] = '\'is a\' 関係の名前'; 12 | $lang['title_key'] = '\'entry title\' 関係の名前'; 13 | $lang['debug'] = 'デバッグ情報の有効化(警告:有効にすると、機密情報を漏洩する可能性がある)'; 14 | 15 | $lang['enable_entry'] = 'データ入力構文の有効化'; 16 | -------------------------------------------------------------------------------- /lib/strata_aggregate.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * This base class defines the methods required by Strata aggregates. 11 | * 12 | * Aggregates are a bit peculiar, as they transform a set of values into 13 | * a set of values. This allows both normal aggregation (many->one), but 14 | * also opens up the option of having (many->many) and (one->many) 15 | * transformations. 16 | */ 17 | class plugin_strata_aggregate { 18 | /** 19 | * Aggregates the values and converts them to a new set of values. 20 | * 21 | * @param values array the set to aggregate 22 | * @param hint string the aggregation hint 23 | * @return an array containing the new values 24 | */ 25 | function aggregate($values, $hint) { 26 | return $values; 27 | } 28 | 29 | /** 30 | * Returns meta-data on the aggregate. This method returns an array with 31 | * the following keys: 32 | * - desc: A human-readable description of the aggregate 33 | * - synthetic: an optional boolean indicating that the aggregate is synthethic 34 | * - hint: an optional string indicating what the aggregate hint's function is 35 | * - tags: an array op applicable tags 36 | * 37 | * @return an array containing the info 38 | */ 39 | function getInfo() { 40 | return array( 41 | 'desc'=>'The generic aggregator. It does nothing.', 42 | 'hint'=>false, 43 | 'synthetic'=>true, 44 | 'tags'=>array() 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/strata_exception.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | class strata_exception extends Exception { 13 | protected $data; 14 | 15 | /** 16 | * Constructor with message and data. 17 | */ 18 | public function __construct($message, $data) { 19 | parent::__construct($message); 20 | $this->data =& $data; 21 | } 22 | 23 | public function getData() { 24 | return $this->data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/strata_querytree_visitor.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | class strata_querytree_visitor { 13 | /** 14 | * Visits a triple pattern. 15 | */ 16 | function visit_tp(&$tp) { 17 | } 18 | 19 | /** 20 | * Visit a filter pattern. 21 | */ 22 | function visit_fp(&$fp) { 23 | } 24 | 25 | /** 26 | * Visit an optional operation. 27 | */ 28 | function visit_opt(&$query) { 29 | $this->dispatch($query['lhs']); 30 | $this->dispatch($query['rhs']); 31 | } 32 | 33 | /** 34 | * Visit an and operation. 35 | */ 36 | function visit_and(&$query) { 37 | $this->dispatch($query['lhs']); 38 | $this->dispatch($query['rhs']); 39 | } 40 | 41 | /** 42 | * Visit a filter operation. 43 | */ 44 | function visit_filter(&$query) { 45 | $this->dispatch($query['lhs']); 46 | foreach($query['rhs'] as &$filter) { 47 | $this->visit_fp($filter); 48 | } 49 | } 50 | 51 | /** 52 | * Visit minus operation. 53 | */ 54 | function visit_minus(&$query) { 55 | $this->dispatch($query['lhs']); 56 | $this->dispatch($query['rhs']); 57 | 58 | } 59 | 60 | /** 61 | * Visit union operation. 62 | */ 63 | function visit_union(&$query) { 64 | $this->dispatch($query['lhs']); 65 | $this->dispatch($query['rhs']); 66 | 67 | } 68 | 69 | /** 70 | * Visit projection and ordering. 71 | */ 72 | function visit_select(&$query) { 73 | $this->dispatch($query['group']); 74 | } 75 | 76 | function dispatch(&$query) { 77 | switch($query['type']) { 78 | case 'select': 79 | return $this->visit_select($query); 80 | case 'union': 81 | return $this->visit_union($query); 82 | case 'minus': 83 | return $this->visit_minus($query); 84 | case 'optional': 85 | return $this->visit_opt($query); 86 | case 'filter': 87 | return $this->visit_filter($query); 88 | case 'triple': 89 | return $this->visit_tp($query); 90 | case 'and': 91 | return $this->visit_and($query); 92 | default: 93 | } 94 | } 95 | 96 | /** 97 | * Visits an abstract query tree to SQL. 98 | */ 99 | function visit(&$query) { 100 | $this->dispatch($query); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/strata_type.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * This base class defines the methods required by Strata types. 11 | * 12 | * There are two kinds of types: normal types and synthetic types. 13 | * Normal types are meant for users, while synthetic types exist to 14 | * keep the plugin working. (i.e., unloadable types are faked by a 15 | * synthetic type, and non-user types should be synthetic). 16 | */ 17 | class plugin_strata_type { 18 | /** 19 | * Renders the value. 20 | * 21 | * @param mode string output format being rendered 22 | * @param renderer ref reference to the current renderer object 23 | * @param triples ref reference to the current triples helper 24 | * @param value string the value to render (this should be a normalized value) 25 | * @param hint string a type hint 26 | * @return true if the mode was handled, false if it was not 27 | */ 28 | function render($mode, &$renderer, &$triples, $value, $hint) { 29 | $renderer->cdata($value); 30 | return true; 31 | } 32 | 33 | /** 34 | * Normalizes the given value 35 | * 36 | * @param value string the value to normalize 37 | * @param hint string a type hint 38 | * @return the normalized value 39 | */ 40 | function normalize($value, $hint) { 41 | return $value; 42 | } 43 | 44 | /** 45 | * Returns meta-data on the type. This method returns an array with 46 | * the following keys: 47 | * - desc: A human-readable description of the type 48 | * - synthetic: an optional boolean indicating that the type is synthethic 49 | * (see class docs for definition of synthetic types) 50 | * - hint: an optional string indicating what the type hint's function is 51 | * - tags: an array op applicable tags 52 | * 53 | * @return an array containing 54 | */ 55 | function getInfo() { 56 | return array( 57 | 'desc'=>'The generic type.', 58 | 'hint'=>false, 59 | 'synthetic'=>true, 60 | 'tags'=>array() 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugin.info.txt: -------------------------------------------------------------------------------- 1 | base strata 2 | author Brend Wanders 3 | email brend+strata@13w.nl 4 | date 2021-12-17 5 | name Strata 6 | desc Semi-Structured data plugin 7 | url http://github.com/bwanders/dokuwiki-strata 8 | -------------------------------------------------------------------------------- /renderer.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | require_once DOKU_INC . 'inc/parser/metadata.php'; 13 | 14 | class renderer_plugin_strata extends Doku_Renderer_metadata { 15 | function getFormat() { 16 | return 'preview_metadata'; 17 | } 18 | 19 | function document_start() { 20 | global $ID; 21 | if(!@file_exists(wikiFN($ID))) { 22 | $this->persistent['date']['created'] = time(); 23 | } 24 | 25 | parent::document_start(); 26 | } 27 | 28 | function document_end() { 29 | global $ID; 30 | $this->meta['date']['modified'] = time(); 31 | parent::document_end(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.fn.sortElements 3 | * -------------- 4 | * @author James Padolsey (http://james.padolsey.com) 5 | * @version 0.11 6 | * @updated 18-MAR-2010 7 | * -------------- 8 | * @param Function comparator: 9 | * Exactly the same behaviour as [1,2,3].sort(comparator) 10 | * 11 | * @param Function getSortable 12 | * A function that should return the element that is 13 | * to be sorted. The comparator will run on the 14 | * current collection, but you may want the actual 15 | * resulting sort to occur on a parent or another 16 | * associated element. 17 | * 18 | * E.g. $('td').sortElements(comparator, function(){ 19 | * return this.parentNode; 20 | * }) 21 | * 22 | * The
) will be sorted instead 23 | * of the
's parent (
itself. 24 | */ 25 | jQuery.fn.sortElements = (function() { 26 | var sort = [].sort; 27 | return function(comparator, getSortable) { 28 | getSortable = getSortable || function() { return this; }; 29 | var placements = this.map(function() { 30 | var sortElement = getSortable.call(this), 31 | parentNode = sortElement.parentNode, 32 | 33 | // Since the element itself will change position, we have 34 | // to have some way of storing it's original position in 35 | // the DOM. The easiest way is to have a 'flag' node: 36 | nextSibling = parentNode.insertBefore( 37 | document.createTextNode(''), 38 | sortElement.nextSibling 39 | ); 40 | 41 | return function() { 42 | if (parentNode === this) { 43 | throw new Error( 44 | "You can't sort elements if any one is a descendant of another." 45 | ); 46 | } 47 | // Insert before flag: 48 | parentNode.insertBefore(this, nextSibling); 49 | // Remove flag: 50 | parentNode.removeChild(nextSibling); 51 | }; 52 | }); 53 | return sort.call(this, comparator).each(function(i) { 54 | placements[i].call(getSortable.call(this)); 55 | }); 56 | }; 57 | })(); 58 | 59 | (function() { 60 | // natural compare 61 | var natcmp = function(s1, s2) { 62 | // 'normalize' the values we're sorting 63 | s1 = s1.replace( /<.*?>/g, "" ).replace('>','<').replace('<','>').replace('&','&'); 64 | s2 = s2.replace( /<.*?>/g, "" ).replace('>','<').replace('<','>').replace('&','&'); 65 | 66 | // do the actual sorting 67 | var n = /^(\d+)(.*)$/; 68 | while (true) { 69 | if (s1 == s2) { return 0; } 70 | if (s1 == '') { return -1; } 71 | if (s2 == '') { return 1; } 72 | var n1 = n.exec(s1); 73 | var n2 = n.exec(s2); 74 | if ( (n1 != null) && (n2 != null) ) { 75 | if (n1[1] != n2[1]) { return n1[1] - n2[1]; } 76 | s1 = n1[2]; 77 | s2 = n2[2]; 78 | } else { 79 | n1 = s1.charCodeAt(0); 80 | n2 = s2.charCodeAt(0); 81 | if (n1 != n2) { return n1 - n2; } 82 | s1 = s1.substr(1); 83 | s2 = s2.substr(1); 84 | } 85 | } 86 | }; 87 | // natural compare right to left (numbers still left to right) 88 | var natcmp_rtl = function(s1, s2) { 89 | // 'normalize' the values we're sorting 90 | s1 = s1.replace( /<.*?>/g, "" ).replace('>','<').replace('<','>').replace('&','&'); 91 | s2 = s2.replace( /<.*?>/g, "" ).replace('>','<').replace('<','>').replace('&','&'); 92 | 93 | // do the actual sorting 94 | var n = /^(.*?)(\d+)$/; 95 | while (true) { 96 | if (s1 == s2) { return 0; } 97 | if (s1 == '') { return -1; } 98 | if (s2 == '') { return 1; } 99 | var n1 = n.exec(s1); 100 | var n2 = n.exec(s2); 101 | if ( (n1 != null) && (n2 != null) ) { 102 | if (n1[2] != n2[2]) { return n1[2] - n2[2]; } 103 | s1 = n1[1]; 104 | s2 = n2[1]; 105 | } else { 106 | n1 = s1.charCodeAt(s1.length - 1); 107 | n2 = s2.charCodeAt(s2.length - 1); 108 | if (n1 != n2) { return n1 - n2; } 109 | s1 = s1.substr(0, s1.length - 1); 110 | s2 = s2.substr(0, s2.length - 1); 111 | } 112 | } 113 | }; 114 | 115 | // generic stable unique function 116 | var unique = function(es) { 117 | var temp = {}; 118 | var result = []; 119 | for(var i = 0; i < es.length; i++) { 120 | var e = es[i]; 121 | if(! (e in temp)) { 122 | result.push(e); 123 | temp[e]=true; 124 | } 125 | } 126 | 127 | return result; 128 | }; 129 | 130 | // multi field compare 131 | var create_item_compare = function(fields, isAscending, sortType) { 132 | return function(item1, item2) { 133 | var valueMap1 = jQuery(item1).data('strata-item-values'); 134 | var valueMap2 = jQuery(item2).data('strata-item-values'); 135 | for (var i = 0; i < fields.length; i++) { 136 | var d = isAscending[i] ? 1 : -1; 137 | var cmp = (sortType[i] == 'r' ? natcmp_rtl : natcmp); 138 | var values1 = valueMap1[fields[i]]; 139 | var values2 = valueMap2[fields[i]]; 140 | var length = Math.min(values1.length, values2.length); 141 | for (var j = 0; j < length; j++) { 142 | var c = cmp(values1[j], values2[j]); 143 | if (c != 0) { 144 | return d * c; 145 | } 146 | } 147 | if (values1.length > values2.length) { 148 | return d * 1; 149 | } else if (values1.length < values2.length) { 150 | return d * -1; 151 | } 152 | } 153 | return parseInt(jQuery(item1).attr('data-strata-order')) - parseInt(jQuery(item2).attr('data-strata-order')); 154 | } 155 | }; 156 | 157 | // Create a filter field of the given type and add it to the given filterElement 158 | var createFilterFieldAndSort = function(filterElement, filterType, filterId, field, sortType, fieldSelector, containerElement, caption, minWidth) { 159 | createItemFilterAndSort(containerElement, filterId, field, fieldSelector, filterType); 160 | if (filterType == 't') { 161 | var input = createFilterTextField(containerElement, filterId, caption); 162 | if (minWidth != undefined) { 163 | jQuery(input).css('min-width', minWidth + 'px'); 164 | } 165 | jQuery(filterElement).append(input); 166 | } else if (filterType == 's' || filterType == 'p' || filterType == 'e') { 167 | var cmp = (sortType == 'r' ? natcmp_rtl : natcmp); 168 | var select = createFilterSelect(containerElement, filterId, fieldSelector, caption, cmp); 169 | jQuery(filterElement).append(select); 170 | } 171 | }; 172 | 173 | // Returns a text input which filters the field belonging to the given filterId 174 | var createFilterTextField = function(element, filterId, caption) { 175 | var input = document.createElement('input'); 176 | input.type = 'text'; 177 | input.size = 1; 178 | input.title = 'Filter on ' + caption; 179 | jQuery(input).keyup(function() { 180 | var val = jQuery(this).val(); 181 | if(val == '') { 182 | delete jQuery(element).data('strata-search')[filterId]; 183 | } else { 184 | jQuery(element).data('strata-search')[filterId] = val.toLowerCase(); 185 | } 186 | strataFilter(element); 187 | toggleFiltered(this); 188 | }); 189 | return input; 190 | }; 191 | 192 | // Returns a select input which filters the field belonging to the given filterId 193 | var createFilterSelect = function(element, filterId, fieldSelector, caption, cmp) { 194 | var select = document.createElement('select'); 195 | jQuery(select).append(''); 196 | var values = []; 197 | jQuery(fieldSelector, element).each(function(_,es) { 198 | var vs = jQuery('*.strata-value', es); 199 | if (vs.length) { 200 | vs.each(function(i, v) { 201 | if (values.indexOf(v.textContent) == -1) { 202 | values.push(v.textContent); 203 | } 204 | }); 205 | } else if (values.indexOf('') == -1) { 206 | values.push(''); 207 | } 208 | }); 209 | values.sort(cmp); 210 | 211 | jQuery.each(values, function(_,v) { 212 | var option = document.createElement('option'); 213 | option.value = v; 214 | option.textContent = v==''?'':v; 215 | if (v == '') { 216 | option.className = 'strata-filter-special'; 217 | } 218 | jQuery(select).append(option); 219 | }); 220 | 221 | jQuery(select).change(function() { 222 | var $option = jQuery(this).find(':selected'); 223 | if($option.attr('data-filter') == 'none') { 224 | delete jQuery(element).data('strata-search')[filterId]; 225 | } else { 226 | jQuery(element).data('strata-search')[filterId] = jQuery(this).val().toLowerCase(); 227 | } 228 | strataFilter(element); 229 | toggleFiltered(this); 230 | }); 231 | return select; 232 | }; 233 | 234 | // Create a filter for every item of the field belonging to the given filterId 235 | var createItemFilterAndSort = function(element, filterId, field, fieldSelector, filterType) { 236 | jQuery('*.strata-item', element).each(function(i, item) { 237 | var values = getValues(item, fieldSelector); 238 | 239 | // Create filter 240 | var filter; 241 | if (filterType == 't') { // substring 242 | // must match at least one value 243 | filter = function(search) { 244 | var result = false; 245 | for (var k = 0; !result && k < values.length; k++) { 246 | result = values[k].indexOf(search) != -1; 247 | } 248 | return result; 249 | }; 250 | } else if (filterType == 'p') { // prefix 251 | // must match at least one value 252 | filter = function(search) { 253 | if (search == '') return jQuery.inArray('', values) != -1; // Filtering for empty prefix is useless, so do exact match 254 | var result = false; 255 | for (var k = 0; !result && k < values.length; k++) { 256 | result = values[k].substr(0, search.length) == search; 257 | } 258 | return result; 259 | }; 260 | } else if (filterType == 'e') { // ending a.k.a. suffix 261 | // must match at least one value 262 | filter = function(search) { 263 | if (search == '') return jQuery.inArray('', values) != -1; // Filtering for empty suffix is useless, so do exact match 264 | var result = false; 265 | for (var k = 0; !result && k < values.length; k++) { 266 | result = values[k].substr(values[k].length - search.length, search.length) == search; 267 | } 268 | return result; 269 | }; 270 | } else { // exact 271 | // must match at least one value 272 | filter = function(search) { 273 | return jQuery.inArray(search, values) != -1; 274 | }; 275 | } 276 | addToItemMap(item, 'strata-item-values', field, values); 277 | addToItemMap(item, 'strata-item-filter', filterId, filter); 278 | }); 279 | }; 280 | 281 | // Get all values for the fields selected by fieldSelector within the given item 282 | function getValues(item, fieldSelector) { 283 | // Return all values of each field and the empty string for fields without values 284 | return jQuery(fieldSelector, item).map(function(_, es) { 285 | var vs = jQuery('*.strata-value', es); 286 | if (vs.length) { 287 | return jQuery.makeArray(vs.map(function(_, v) { 288 | return v.textContent.toLowerCase(); 289 | })); 290 | } else { 291 | return ''; 292 | } 293 | }); 294 | } 295 | 296 | // Store data of the given field for the given item 297 | var addToItemMap = function(item, key, id, values) { 298 | var valueMap = jQuery(item).data(key); 299 | if (valueMap == undefined) { 300 | valueMap = {}; 301 | jQuery(item).data(key, valueMap); 302 | } 303 | valueMap[id] = values; 304 | }; 305 | 306 | var sortGeneric = function(element, fieldlist) { 307 | var fields = []; 308 | var isAscending = []; 309 | var sortType = []; 310 | var items = jQuery('li', fieldlist); 311 | for (var i = 0; i < items.length && jQuery(items[i]).attr('data-field') != undefined; i++) { 312 | fields.push(jQuery(items[i]).attr('data-field')); 313 | isAscending.push(jQuery('.strata-ui-sort-direction', items[i]).attr('data-strata-sort-direction') == 'asc'); 314 | sortType.push(jQuery(items[i]).data('strata-sort-type')); 315 | } 316 | jQuery('.strata-item', element).sortElements(create_item_compare(fields, isAscending, sortType)); 317 | }; 318 | 319 | var sortTable = function(element, field, isAdditional) { 320 | var fields = jQuery(element).data('strata-sort-fields'); 321 | var isAscending = jQuery(element).data('strata-sort-directions'); 322 | var sortType = []; 323 | if (fields[0] == field) { 324 | if (isAscending[0]) { // Change sort direction 325 | isAscending[0] = false; 326 | } else { // Remove from sort 327 | fields.splice(0, 1); 328 | isAscending.splice(0, 1); 329 | } 330 | } else if (isAdditional) { // Add as sort field 331 | var i = fields.indexOf(field); 332 | if (i >= 0) { 333 | fields.splice(i, 1); 334 | isAscending.splice(i, 1); 335 | } 336 | fields.unshift(field); 337 | isAscending.unshift(true); 338 | } else { // Replace sort with given field 339 | fields.splice(0, fields.length, field); 340 | isAscending.splice(0, fields.length, true); 341 | } 342 | var sort = jQuery(element).attr('data-strata-ui-sort'); 343 | jQuery('th', element).removeAttr('data-strata-sort').removeAttr('data-strata-sort-direction'); 344 | jQuery('td', element).removeAttr('data-strata-sort').removeAttr('data-strata-sort-direction'); 345 | for (var i = 0; i < fields.length; i++) { 346 | var col = fields[i]; 347 | jQuery('.col' + col, element).attr('data-strata-sort', i); 348 | jQuery('.col' + col, element).attr('data-strata-sort-direction', isAscending[i] ? 'asc' : 'desc'); 349 | sortType.push(sort[col]); 350 | } 351 | jQuery('.strata-item', element).sortElements(create_item_compare(fields, isAscending, sortType)); 352 | }; 353 | 354 | // UI initialization 355 | jQuery(document).ready(function() { 356 | // Table UI initialization 357 | jQuery('div.strata-container-table[data-strata-ui-ui="table"]').each(function(i, div) { 358 | // Do not make this a dataTable if a colspan is used somewhere (Colspans are only generated by strata when errors occur) 359 | if (jQuery('table tbody td[colspan][colspan != 1]', div).length > 0) { 360 | return; 361 | } 362 | 363 | // Set filter to empty set 364 | jQuery(div).data('strata-search', {}); 365 | 366 | var filterColumns = jQuery(div).attr('data-strata-ui-filter'); 367 | var sortColumns = jQuery(div).attr('data-strata-ui-sort'); 368 | 369 | // Create sort and filter fields for each column 370 | var tr = document.createElement('tr'); 371 | jQuery(tr).addClass('filter'); 372 | var thead = jQuery('thead', div); 373 | var headers = jQuery('tr.row0 th', thead); 374 | headers.each(function(i, td) { 375 | var field = jQuery('.strata-caption', td).attr('data-field'); 376 | var th = document.createElement('th'); // Filter field 377 | if (field != undefined) { // Is there a field to sort/filter on? 378 | // Create sort 379 | if (sortColumns.charAt(i) != 'n') { 380 | jQuery(td).addClass('sorting'); 381 | jQuery(td).click(function(e) { 382 | sortTable(div, i, e.shiftKey); 383 | }); 384 | } 385 | // Create filter 386 | var fieldSelector = '.col' + i + ' *.strata-field'; 387 | createFilterFieldAndSort(th, filterColumns.charAt(i), i, i, sortColumns.charAt(i), fieldSelector, div, td.textContent); 388 | } 389 | jQuery(tr).append(th); 390 | }); 391 | jQuery(thead).append(tr); 392 | 393 | // Set column widths 394 | jQuery('thead tr.row0 th', div).each( 395 | function(i, th) { 396 | // Set the width of a column to its initial width, which is the width of the widest row. 397 | // This avoids resizing when filtering hides long rows in the table. 398 | var width = jQuery(th).width(); 399 | jQuery(th).css('min-width', width + 'px'); 400 | } 401 | ); 402 | 403 | // Set data for sort 404 | jQuery(div).data('strata-sort-fields', []); 405 | jQuery(div).data('strata-sort-directions', []); 406 | 407 | // Allow switching to alternate table view with the meta key 408 | jQuery(thead).click(function(e) { 409 | if (e.metaKey) { 410 | jQuery(div).toggleClass('strata-ui-filter'); 411 | } 412 | }); 413 | }); 414 | 415 | // Generic UI initialization 416 | jQuery('div.strata-container[data-strata-ui-ui="generic"]').each(function(i, div) { 417 | // Set filter to empty set 418 | jQuery(div).data('strata-search', {}); 419 | 420 | var filterColumns = jQuery(div).attr('data-strata-ui-filter'); 421 | var sortColumns = jQuery(div).attr('data-strata-ui-sort'); 422 | 423 | // Create sort and filter fields for each column 424 | var list = document.createElement('ul'); 425 | jQuery(list).addClass('filter') 426 | .mouseenter(function(){ jQuery(div).toggleClass('section_highlight', true); }) 427 | .mouseleave(function(){ jQuery(div).toggleClass('section_highlight', false); }); 428 | 429 | var li = document.createElement('li'); 430 | jQuery(li).addClass('ui-state-highlight strata-ui-eos'); 431 | jQuery(li).append(document.createTextNode('End of sort order')); 432 | jQuery(list).append(li); 433 | var lastSortable = li; 434 | 435 | // Collect all sort and filter fields 436 | var fields = {}; 437 | var fieldOrder = []; 438 | jQuery('.strata-caption', div).each(function(i, captionElement) { 439 | if (sortColumns.charAt(i) != 'n' || filterColumns.charAt(i) != 'n') { 440 | var field = jQuery(captionElement).attr('data-field'); 441 | var minWidth = Math.max.apply(Math, jQuery('*.strata-field[data-field="' + field + '"] .strata-value', div).map(function(_, v) { 442 | return jQuery(v).width(); 443 | })); 444 | var f; 445 | if (field in fields) { 446 | f = fields[field]; 447 | f.caption.push(captionElement.textContent); 448 | if (f.sortType == 'n') { 449 | f.sortType = sortColumns.charAt(i); 450 | } 451 | f.minWidth = Math.max(f.minWidth, minWidth); 452 | } else { 453 | f = { 454 | 'field': field, 455 | 'caption': [captionElement.textContent], 456 | 'sortType': sortColumns.charAt(i), 457 | 'minWidth': minWidth, 458 | 'filters': [] 459 | }; 460 | fields[field] = f; 461 | fieldOrder.push(f); 462 | } 463 | if (filterColumns.charAt(i) != 'n') { 464 | f.filters.push(filterColumns.charAt(i)); 465 | } 466 | } 467 | }); 468 | // Create the collected fields 469 | for (var i = 0; i < fieldOrder.length; i++) { 470 | var f = fieldOrder[i]; 471 | var caption = unique(f.caption).join(' / '); 472 | var li = document.createElement('li'); 473 | jQuery(li).addClass('ui-state-default'); 474 | jQuery(li).attr('data-field', f.field); 475 | jQuery(li).append(document.createTextNode(caption)); 476 | var fieldSelector = '*.strata-field[data-field="' + f.field + '"]'; 477 | if (f.filters.length) { 478 | jQuery(li).append(' '); 479 | } 480 | for (var j = 0; j < f.filters.length; j++) { 481 | createFilterFieldAndSort(li, f.filters[j], i + '_' + j, f.field, f.sortType, fieldSelector, div, caption, f.minWidth); 482 | } 483 | if (f.sortType != 'n') { 484 | jQuery(li).data('strata-sort-type', f.sortType); 485 | var span = document.createElement('span'); 486 | jQuery(span).addClass('strata-ui-sort-direction'); 487 | jQuery(span).attr('data-strata-sort-direction', 'asc'); 488 | jQuery(span).append(' '); 489 | jQuery(li).append(span); 490 | jQuery(span).click(function(e) { 491 | var dir = jQuery(this).attr('data-strata-sort-direction') == 'asc' ? 'desc' : 'asc'; 492 | jQuery(this).attr('data-strata-sort-direction', dir); 493 | sortGeneric(div, list); 494 | }); 495 | if (f.filters.length == 0) { // No sort data was stored yet, do it now 496 | jQuery('*.strata-item', div).each(function(i, item) { 497 | addToItemMap(item, 'strata-item-values', f.field, getValues(item, fieldSelector)); 498 | }); 499 | } 500 | } else { 501 | jQuery(li).append(' '); 502 | } 503 | if (f.sortType == 'n') { 504 | jQuery(li).addClass('strata-no-sort'); 505 | jQuery(list).append(li); 506 | } else { 507 | jQuery(lastSortable).after(li); 508 | lastSortable = li; 509 | } 510 | } 511 | jQuery(div).prepend(list); 512 | 513 | // Set data for sort 514 | jQuery(div).data('strata-sort-fields', []); 515 | jQuery(div).data('strata-sort-directions', []); 516 | 517 | jQuery(list).sortable({ 518 | items: "li:not(.strata-no-sort)", 519 | placeholder: "ui-state-default ui-state-disabled ui-drop-target", 520 | start: function(e, ui) { 521 | jQuery(ui.placeholder).css('min-width', jQuery(ui.item).width() + 'px'); 522 | }, 523 | update: function(e, ui) { 524 | sortGeneric(div, list); 525 | } 526 | }); 527 | }); 528 | }); 529 | 530 | // Filter every strata-item in the given element based on its filter 531 | var strataFilter = function(element) { 532 | var search = jQuery(element).data('strata-search'); 533 | // Traverse all items (rows) that can be filtered 534 | jQuery('*.strata-item', element).each(function(_, item) { 535 | // Traverse all fields on which a filter is applied, filter must match all fields 536 | var filterMap = jQuery(item).data('strata-item-filter'); 537 | var matchesAllFields = true; 538 | for (filterId in search) { 539 | var filter = filterMap[filterId]; 540 | if (!filter(search[filterId])) { 541 | matchesAllFields = false; 542 | break; 543 | } 544 | } 545 | jQuery(item).toggleClass('hidden', !matchesAllFields); 546 | }); 547 | }; 548 | 549 | var toggleFiltered = function(tableElement) { 550 | var tr = jQuery(tableElement).closest('tr.filter'); 551 | //console.log(Object.keys(...).length); 552 | var isFiltered = false; 553 | tr.find('input').each(function(_, input) { 554 | isFiltered = isFiltered || (input.value != ''); 555 | }); 556 | tr.find('select').each(function(_, select) { 557 | isFiltered = isFiltered || (jQuery(select).val() != ''); 558 | }); 559 | tr.toggleClass('isFiltered', isFiltered); 560 | }; 561 | 562 | })(); 563 | -------------------------------------------------------------------------------- /sql/setup-mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE data ( 2 | subject TEXT NOT NULL, 3 | predicate TEXT NOT NULL, 4 | object TEXT NOT NULL, 5 | graph TEXT NOT NULL 6 | ) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ENGINE InnoDB; 7 | 8 | -- index for subject-primary retrieval (index prefixes: s, sp) 9 | CREATE INDEX idx_spo ON data(subject(32), predicate(32), object(32)); -- Prefix length is arbitrary 10 | 11 | -- index for predicate-primary retrieval (i.e. property fetch) 12 | CREATE INDEX idx_pso ON data(predicate(32), subject(32), object(32)); -- Prefix length is arbitrary 13 | -------------------------------------------------------------------------------- /sql/setup-pgsql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE data ( 2 | subject TEXT NOT NULL, 3 | predicate TEXT NOT NULL, 4 | object TEXT NOT NULL, 5 | graph TEXT NOT NULL 6 | ); 7 | 8 | -- index for subject-primary retrieval (index prefixes: s, sp) 9 | CREATE INDEX idx_spo ON data(lower(subject), lower(predicate), lower(object)); 10 | 11 | -- index for predicate-primary retrieval (i.e. property fetch) 12 | CREATE INDEX idx_pso ON data(lower(predicate), lower(subject), lower(object)); 13 | -------------------------------------------------------------------------------- /sql/setup-sqlite.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE data ( 2 | subject TEXT NOT NULL COLLATE NOCASE, 3 | predicate TEXT NOT NULL COLLATE NOCASE, 4 | object TEXT NOT NULL COLLATE NOCASE, 5 | graph TEXT NOT NULL COLLATE NOCASE 6 | ); 7 | 8 | -- index for subject-primary retrieval (index prefixes: s, sp) 9 | CREATE INDEX idx_spo ON data(subject, predicate, object); 10 | 11 | -- index for predicate-primary retrieval (i.e. property fetch) 12 | CREATE INDEX idx_pso ON data(predicate, subject, object); 13 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Styling for error display 3 | */ 4 | 5 | .dokuwiki div.strata-debug-message { /* the error message */ 6 | color: __text__; 7 | background-image: url(../../images/error.png); 8 | background-repeat: no-repeat; 9 | background-position: 8px 50%; 10 | background-color: #FCC; 11 | 12 | margin: 0; 13 | padding: 0.4em 0.4em 0.4em 32px; 14 | 15 | border: 1px solid; 16 | border-color: #FAA; 17 | border-radius: 5px; 18 | border-top-right-radius: 5px; 19 | border-top-left-radius: 5px; 20 | } 21 | 22 | .dokuwiki div.strata-debug-message.strata-debug-continued { /* error message is followed by something */ 23 | border-bottom-right-radius: 0; 24 | border-bottom-left-radius: 0; 25 | } 26 | 27 | .dokuwiki div.strata-debug { /* the line container */ 28 | line-height: normal; 29 | font-family: monospace; 30 | background-color: __background_site__; 31 | padding: 0.5em; 32 | 33 | border: 1px solid __border__; 34 | border-top: 0; 35 | border-bottom-right-radius: 5px; 36 | border-bottom-left-radius: 5px; 37 | 38 | box-shadow: 0 0 0.5em __border__ inset; 39 | } 40 | .dokuwiki div.strata-debug div.strata-debug-line { /* normal line */ 41 | white-space: pre; 42 | padding: 0 2px 0 2px; 43 | } 44 | 45 | .dokuwiki div.strata-debug div.strata-debug-highlight { /* The highlighted lines container */ 46 | background-color: #FCC; 47 | color: __text__; 48 | border: 1px #faa solid; 49 | border-radius: 3px; 50 | } 51 | 52 | /** 53 | * Entry styling 54 | */ 55 | .dokuwiki .strata-data-fragment-link-previous { 56 | float: left; 57 | } 58 | .dokuwiki .strata-data-fragment-link-next { 59 | float: right; 60 | } 61 | 62 | /* Column filter style */ 63 | .strata-container-table tr.filter input { 64 | background-color: __background__; 65 | border: 1px solid silver; 66 | font-style: italic; 67 | width: 100%; 68 | } 69 | 70 | .strata-container-table tr.filter { 71 | display: none; 72 | } 73 | 74 | .strata-container-table tr.filter.isFiltered, .strata-container.strata-ui-filter table tr.filter { 75 | display: table-row; 76 | } 77 | 78 | /* Sort style */ 79 | .strata-ui-sort-direction, 80 | .strata-ui-eos ~ li .strata-ui-sort-direction, 81 | .strata-container-table:hover .sorting { 82 | background-image: url('images/sort_both.png'); 83 | } 84 | 85 | .strata-ui-sort-direction[data-strata-sort-direction="asc"], 86 | .strata-container-table .sorting[data-strata-sort-direction="asc"], 87 | .strata-container-table:hover .sorting[data-strata-sort-direction="asc"] { 88 | background-image: url('images/sort_asc.png'); 89 | } 90 | 91 | .strata-ui-sort-direction[data-strata-sort-direction="desc"], 92 | .strata-container-table .sorting[data-strata-sort-direction="desc"], 93 | .strata-container-table:hover .sorting[data-strata-sort-direction="desc"] { 94 | background-image: url('images/sort_desc.png'); 95 | } 96 | 97 | /* Column sort style */ 98 | .strata-container-table thead tr th.sorting { 99 | padding-right: 19px; 100 | cursor: pointer; 101 | background-position: right center; 102 | background-repeat: no-repeat; 103 | } 104 | 105 | .strata-container-table thead tr input, 106 | .strata-container-table thead tr select { 107 | min-width: 100%; 108 | } 109 | 110 | .strata-container-table td[data-strata-sort="0"] { 111 | background-color: #EAEBFF; 112 | } 113 | 114 | .strata-container-table td[data-strata-sort="1"] { 115 | background-color: #F2F3FF; 116 | } 117 | 118 | .strata-container-table td[data-strata-sort="2"] { 119 | background-color: #F9F9FF; 120 | } 121 | 122 | /* Generic sort and filter style */ 123 | .strata-ui-sort-direction { 124 | display: inline-block; 125 | width: 19px; 126 | margin-left: 4px; 127 | cursor: pointer; 128 | background-position: center center; 129 | background-repeat: no-repeat; 130 | } 131 | 132 | .strata-ui-eos ~ li .strata-ui-sort-direction { 133 | cursor: auto; 134 | } 135 | 136 | .strata-container ul.filter { 137 | list-style-type: none; 138 | padding: 0; 139 | overflow: auto; 140 | border-radius: 5px; 141 | box-shadow: 0 0 5px __border__ inset; 142 | padding: 5px; 143 | } 144 | 145 | .page .strata-container ul.filter li { 146 | margin: 1.5px 1.5px 1.5px 1.5px; 147 | padding: 4px 4px 4px 4px; 148 | height: 21px; 149 | display: inline-block; 150 | border-radius: 2px; 151 | color: @ini_text; 152 | } 153 | 154 | .strata-container ul.filter li:not(.strata-no-sort):before { 155 | content: "|||| "; 156 | cursor: move; 157 | text-shadow: 1px 1px white; 158 | } 159 | 160 | .strata-container ul.filter li.ui-drop-target:before { 161 | content: "Drop here"; 162 | font-style: italic; 163 | padding: 4px 7px 4px 7px; 164 | } 165 | 166 | .strata-container .filter option.strata-filter-special { 167 | font-style: italic; 168 | } 169 | 170 | .strata-container .filter li input, 171 | .strata-container .filter li select { 172 | margin-left: 2px; 173 | } 174 | -------------------------------------------------------------------------------- /syntax/entry.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | if (!defined('DOKU_INC')) die('Meh.'); 10 | 11 | /** 12 | * Data entry syntax for dedicated data blocks. 13 | */ 14 | class syntax_plugin_strata_entry extends DokuWiki_Syntax_Plugin { 15 | protected static $previewMetadata = array(); 16 | 17 | function __construct() { 18 | $this->syntax =& plugin_load('helper', 'strata_syntax'); 19 | $this->util =& plugin_load('helper', 'strata_util'); 20 | $this->triples =& plugin_load('helper', 'strata_triples'); 21 | } 22 | 23 | function getType() { 24 | return 'substition'; 25 | } 26 | 27 | function getPType() { 28 | return 'block'; 29 | } 30 | 31 | function getSort() { 32 | return 450; 33 | } 34 | 35 | function connectTo($mode) { 36 | if($this->getConf('enable_entry')) { 37 | $this->Lexer->addSpecialPattern(']+?)?(?: *#[^>]*?)?>\s*?\n(?:.*?\n)*?\s*?',$mode, 'plugin_strata_entry'); 38 | } 39 | } 40 | 41 | function handle($match, $state, $pos, Doku_Handler $handler) { 42 | $result = array( 43 | 'entry'=>'', 44 | 'data'=> array( 45 | $this->util->getIsaKey(false) => array(), 46 | $this->util->getTitleKey(false) => array() 47 | ) 48 | ); 49 | 50 | // allow for preprocessing by a subclass 51 | $match = $this->preprocess($match, $state, $pos, $handler, $result); 52 | 53 | $lines = explode("\n",$match); 54 | $header = trim(array_shift($lines)); 55 | $footer = trim(array_pop($lines)); 56 | 57 | 58 | // allow subclasses to mangle header 59 | $header = $this->handleHeader($header, $result); 60 | 61 | // extract header, and match it to get classes and fragment 62 | preg_match('/^( +[^#>]+)?(?: *#([^>]*?))?$/', $header, $header); 63 | 64 | // process the classes into triples 65 | foreach(preg_split('/\s+/',trim($header[1])) as $class) { 66 | if($class == '') continue; 67 | $result['data'][$this->util->getIsaKey(false)][] = array('value'=>$class,'type'=>'text', 'hint'=>null); 68 | } 69 | 70 | // process the fragment if necessary 71 | $result['entry'] = $header[2]; 72 | $result['position'] = $pos; 73 | if($result['entry'] != '') { 74 | $result['title candidate'] = array('value'=>$result['entry'], 'type'=>'text', 'hint'=>null); 75 | } 76 | 77 | // parse tree 78 | $tree = $this->syntax->constructTree($lines,'data entry'); 79 | 80 | // allow subclasses first pick in the tree 81 | $this->handleBody($tree, $result); 82 | 83 | // fetch all lines 84 | $lines = $this->syntax->extractText($tree); 85 | 86 | // sanity check 87 | if(count($tree['cs'])) { 88 | msg(sprintf($this->syntax->getLang('error_entry_block'), ($tree['cs'][0]['tag']?sprintf($this->syntax->getLang('named_group'),utf8_tohtml(hsc($tree['cs'][0]['tag']))):$this->syntax->getLang('unnamed_group')), utf8_tohtml(hsc($result['entry']))),-1); 89 | return array(); 90 | } 91 | 92 | $p = $this->syntax->getPatterns(); 93 | 94 | // now handle all lines 95 | foreach($lines as $line) { 96 | $line = $line['text']; 97 | // match a "property_type(hint)*: value" pattern 98 | // (the * is only used to indicate that the value is actually a comma-seperated list) 99 | // [grammar] ENTRY := PREDICATE TYPE? '*'? ':' ANY 100 | if(preg_match("/^({$p->predicate})\s*({$p->type})?\s*(\*)?\s*:\s*({$p->any}?)$/",$line,$parts)) { 101 | // assign useful names 102 | list(, $property, $ptype, $multi, $values) = $parts; 103 | list($type,$hint) = $p->type($ptype); 104 | 105 | // trim property so we don't get accidental 'name ' keys 106 | $property = utf8_trim($property); 107 | 108 | // lazy create key bucket 109 | if(!isset($result['data'][$property])) { 110 | $result['data'][$property] = array(); 111 | } 112 | 113 | // determine values, splitting on commas if necessary 114 | $values = ($multi == '*') ? explode(',',$values) : array($values); 115 | 116 | // generate triples from the values 117 | foreach($values as $v) { 118 | $v = utf8_trim($v); 119 | if($v == '') continue; 120 | // replace the [[]] quasi-magic token with the empty string 121 | if($v == '[[]]') $v = ''; 122 | if(!isset($type) || $type == '') { 123 | list($type, $hint) = $this->util->getDefaultType(); 124 | } 125 | $result['data'][$property][] = array('value'=>$v,'type'=>$type,'hint'=>($hint?:null)); 126 | } 127 | } else { 128 | msg(sprintf($this->syntax->getLang('error_entry_line'), utf8_tohtml(hsc($line))),-1); 129 | } 130 | } 131 | 132 | // normalize data: 133 | // - Normalize all values 134 | $buckets = $result['data']; 135 | $result['data'] = array(); 136 | 137 | foreach($buckets as $property=>&$bucket) { 138 | // normalize the predicate 139 | $property = $this->util->normalizePredicate($property); 140 | 141 | // process all triples 142 | foreach($bucket as &$triple) { 143 | // normalize the value 144 | $type = $this->util->loadType($triple['type']); 145 | $triple['value'] = $type->normalize($triple['value'], $triple['hint']); 146 | 147 | // lazy create property bucket 148 | if(!isset($result['data'][$property])) { 149 | $result['data'][$property] = array(); 150 | } 151 | 152 | $result['data'][$property][] = $triple; 153 | } 154 | } 155 | 156 | 157 | // normalize title candidate 158 | if(!empty($result['title candidate'])) { 159 | $type = $this->util->loadType($result['title candidate']['type']); 160 | $result['title candidate']['value'] = $type->normalize($result['title candidate']['value'], $result['title candidate']['hint']); 161 | } 162 | 163 | $footer = $this->handleFooter($footer, $result); 164 | 165 | return $result; 166 | } 167 | 168 | /** 169 | * Handles the whole match. This method is called before any processing 170 | * is done by the actual class. 171 | * 172 | * @param match string the complete match 173 | * @param state the parser state 174 | * @param pos the position in the source 175 | * @param the handler object 176 | * @param result array the result array passed to the render method 177 | * @return a preprocessed string 178 | */ 179 | function preprocess($match, $state, $pos, &$handler, &$result) { 180 | return $match; 181 | } 182 | 183 | /** 184 | * Handles the header of the syntax. This method is called before 185 | * the header is handled. 186 | * 187 | * @param header string the complete header 188 | * @param result array the result array passed to the render method 189 | * @return a string containing the unhandled parts of the header 190 | */ 191 | function handleHeader($header, &$result) { 192 | // remove prefix and suffix 193 | return preg_replace('/(^$)/','',$header); 194 | } 195 | 196 | /** 197 | * Handles the body of the syntax. This method is called before any 198 | * of the body is handled. 199 | * 200 | * @param tree array the parsed tree 201 | * @param result array the result array passed to the render method 202 | */ 203 | function handleBody(&$tree, &$result) { 204 | } 205 | 206 | /** 207 | * Handles the footer of the syntax. This method is called after the 208 | * data has been parsed and normalized. 209 | * 210 | * @param footer string the footer string 211 | * @param result array the result array passed to the render method 212 | * @return a string containing the unhandled parts of the footer 213 | */ 214 | function handleFooter($footer, &$result) { 215 | return ''; 216 | } 217 | 218 | 219 | protected function getPositions($data) { 220 | global $ID; 221 | 222 | // determine positions of other data entries 223 | // (self::$previewMetadata is only filled if a preview_metadata was run) 224 | if(isset(self::$previewMetadata[$ID])) { 225 | $positions = self::$previewMetadata[$ID]['strata']['positions']; 226 | } else { 227 | $positions = p_get_metadata($ID, 'strata positions'); 228 | } 229 | 230 | // only read positions if we have them 231 | if(is_array($positions) && isset($positions[$data['entry']])) { 232 | $positions = $positions[$data['entry']]; 233 | $currentPosition = array_search($data['position'],$positions); 234 | $previousPosition = isset($positions[$currentPosition-1])?'data_fragment_'.$positions[$currentPosition-1]:null; 235 | $nextPosition = isset($positions[$currentPosition+1])?'data_fragment_'.$positions[$currentPosition+1]:null; 236 | $currentPosition = 'data_fragment_'.$positions[$currentPosition]; 237 | } 238 | 239 | return array($currentPosition, $previousPosition, $nextPosition); 240 | } 241 | 242 | function render($mode, Doku_Renderer $R, $data) { 243 | global $ID; 244 | 245 | if($data == array()) { 246 | return false; 247 | } 248 | 249 | if($mode == 'xhtml' || $mode == 'odt') { 250 | list($currentPosition, $previousPosition, $nextPosition) = $this->getPositions($data); 251 | // render table header 252 | if($mode == 'xhtml') { $R->doc .= '
'; } 253 | if($mode == 'odt' && isset($currentPosition) && method_exists ($R, 'insertBookmark')) { 254 | $R->insertBookmark($currentPosition, false); 255 | } 256 | $R->table_open(); 257 | $R->tablerow_open(); 258 | $R->tableheader_open(2); 259 | 260 | // determine actual header text 261 | $heading = ''; 262 | if(isset($data['data'][$this->util->getTitleKey()])) { 263 | // use title triple if possible 264 | $heading = $data['data'][$this->util->getTitleKey()][0]['value']; 265 | } elseif (!empty($data['title candidate'])) { 266 | // use title candidate if possible 267 | $heading = $data['title candidate']['value']; 268 | } else { 269 | if(useHeading('content')) { 270 | // fall back to page title, depending on wiki configuration 271 | $heading = p_get_first_heading($ID); 272 | } 273 | 274 | if(!$heading) { 275 | // use page id if all else fails 276 | $heading = noNS($ID); 277 | } 278 | } 279 | $R->cdata($heading); 280 | 281 | // display a comma-separated list of classes if the entry has classes 282 | if(isset($data['data'][$this->util->getIsaKey()])) { 283 | $R->emphasis_open(); 284 | $R->cdata(' ('); 285 | $values = $data['data'][$this->util->getIsaKey()]; 286 | $this->util->openField($mode, $R, $this->util->getIsaKey()); 287 | for($i=0;$icdata(', '); 290 | $type = $this->util->loadType($triple['type']); 291 | $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $type, $triple['hint']); 292 | } 293 | $this->util->closeField($mode, $R); 294 | $R->cdata(')'); 295 | $R->emphasis_close(); 296 | } 297 | $R->tableheader_close(); 298 | $R->tablerow_close(); 299 | 300 | // render a row for each key, displaying the values as comma-separated list 301 | foreach($data['data'] as $key=>$values) { 302 | // skip isa and title keys 303 | if($key == $this->util->getTitleKey() || $key == $this->util->getIsaKey()) continue; 304 | 305 | // render row header 306 | $R->tablerow_open(); 307 | $R->tableheader_open(); 308 | $this->util->renderPredicate($mode, $R, $this->triples, $key); 309 | $R->tableheader_close(); 310 | 311 | // render row content 312 | $R->tablecell_open(); 313 | $this->util->openField($mode, $R, $key); 314 | for($i=0;$icdata(', '); 317 | $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $triple['hint']); 318 | } 319 | $this->util->closeField($mode, $R); 320 | $R->tablecell_close(); 321 | $R->tablerow_close(); 322 | } 323 | 324 | if($previousPosition || $nextPosition) { 325 | $R->tablerow_open(); 326 | $R->tableheader_open(2); 327 | if($previousPosition) { 328 | if($mode == 'xhtml') { $R->doc .= ''; } 329 | $R->locallink($previousPosition, $this->util->getLang('data_entry_previous')); 330 | if($mode == 'xhtml') { $R->doc .= ''; } 331 | } 332 | $R->cdata(' '); 333 | if($nextPosition) { 334 | if($mode == 'xhtml') { $R->doc .= ''; } 335 | $R->locallink($nextPosition, $this->util->getLang('data_entry_next')); 336 | if($mode == 'xhtml') { $R->doc .= ''; } 337 | } 338 | $R->tableheader_close(); 339 | $R->tablerow_close(); 340 | } 341 | 342 | $R->table_close(); 343 | if($mode == 'xhtml') { $R->doc .= '
'; } 344 | 345 | return true; 346 | 347 | } elseif($mode == 'metadata' || $mode == 'preview_metadata') { 348 | $triples = array(); 349 | $subject = $ID.'#'.$data['entry']; 350 | 351 | // resolve the subject to normalize everything 352 | resolve_pageid(getNS($ID),$subject,$exists); 353 | 354 | $titleKey = $this->util->getTitleKey(); 355 | 356 | $fixTitle = false; 357 | 358 | // we only use the title determination if no explicit title was given 359 | if(empty($data['data'][$titleKey])) { 360 | if(!empty($data['title candidate'])) { 361 | // we have a candidate from somewhere 362 | $data['data'][$titleKey][] = $data['title candidate']; 363 | } else { 364 | if(!empty($R->meta['title'])) { 365 | // we do not have a candidate, so we use the page title 366 | // (this is possible because fragments set the candidate) 367 | $data['data'][$titleKey][] = array( 368 | 'value'=>$R->meta['title'], 369 | 'type'=>'text', 370 | 'hint'=>null 371 | ); 372 | } else { 373 | // we were added before the page title is known 374 | // however, we do require a page title (iff we actually store data) 375 | $fixTitle = true; 376 | } 377 | } 378 | } 379 | 380 | // store positions information 381 | if($mode == 'preview_metadata') { 382 | self::$previewMetadata[$ID]['strata']['positions'][$data['entry']][] = $data['position']; 383 | } else { 384 | $R->meta['strata']['positions'][$data['entry']][] = $data['position']; 385 | } 386 | 387 | // process triples 388 | foreach($data['data'] as $property=>$bucket) { 389 | $this->util->renderPredicate($mode, $R, $this->triples, $property); 390 | 391 | foreach($bucket as $triple) { 392 | // render values for things like backlinks 393 | $type = $this->util->loadType($triple['type']); 394 | $type->render($mode, $R, $this->triples, $triple['value'], $triple['hint']); 395 | 396 | // prepare triples for storage 397 | $triples[] = array('subject'=>$subject, 'predicate'=>$property, 'object'=>$triple['value']); 398 | } 399 | } 400 | 401 | // we're done if nodata is flagged. 402 | if(!isset($R->info['data']) || $R->info['data']==true) { 403 | // batch-store triples if we're allowed to store 404 | $this->triples->addTriples($triples, $ID); 405 | 406 | // set flag for title addendum 407 | if($fixTitle) { 408 | $R->meta['strata']['fixTitle'] = true; 409 | } 410 | } 411 | 412 | return true; 413 | } 414 | 415 | return false; 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /syntax/info.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | /** 13 | * Simple plugin to list the available types. This plugin uses 14 | * the same syntax as the info plugin, but only accepts a specific 15 | * info category. 16 | */ 17 | class syntax_plugin_strata_info extends DokuWiki_Syntax_Plugin { 18 | public function __construct() { 19 | $this->util =& plugin_load('helper', 'strata_util'); 20 | } 21 | 22 | public function getType() { 23 | return 'substition'; 24 | } 25 | 26 | public function getPType() { 27 | return 'block'; 28 | } 29 | 30 | public function getSort() { 31 | // sort just before info plugin 32 | return 154; 33 | } 34 | 35 | 36 | public function connectTo($mode) { 37 | $this->Lexer->addSpecialPattern('~~INFO:stratatypes~~',$mode,'plugin_strata_info'); 38 | $this->Lexer->addSpecialPattern('~~INFO:strataaggregates~~',$mode,'plugin_strata_info'); 39 | } 40 | 41 | public function handle($match, $state, $pos, Doku_Handler $handler){ 42 | $data = array(); 43 | preg_match('/~~INFO:strata(type|aggregate)s~~/',$match, $captures); 44 | list(,$kind) = $captures; 45 | 46 | // get a list of all types... 47 | foreach(glob(DOKU_PLUGIN."*/${kind}s/*.php") as $type) { 48 | if(preg_match("@/([^/]+)/${kind}s/([^/]+)\.php@",$type,$matches)) { 49 | // ...load each type... 50 | switch($kind) { 51 | case 'type': $meta = $this->util->loadType($matches[2])->getInfo(); break; 52 | case 'aggregate': $meta = $this->util->loadAggregate($matches[2])->getInfo(); break; 53 | } 54 | 55 | // ...and check if it's synthetic (i.e., not user-usable) 56 | if(!isset($meta['synthetic']) || !$meta['synthetic']) { 57 | $data[] = array( 58 | 'name'=>$matches[2], 59 | 'plugin'=>$matches[1], 60 | 'meta'=>$meta 61 | ); 62 | } 63 | } 64 | } 65 | 66 | usort($data, array($this,'_compareNames')); 67 | 68 | return array($kind,$data); 69 | } 70 | 71 | function _compareNames($a, $b) { 72 | return strcmp($a['name'], $b['name']); 73 | } 74 | 75 | public function render($mode, Doku_Renderer $R, $data) { 76 | if($mode == 'xhtml' || $mode == 'odt') { 77 | list($kind, $items) = $data; 78 | 79 | $R->listu_open(); 80 | foreach($items as $data){ 81 | $R->listitem_open(1); 82 | $R->listcontent_open(); 83 | 84 | $R->strong_open(); 85 | $R->cdata($data['name']); 86 | $R->strong_close(); 87 | 88 | if($data['meta']['hint']) { 89 | $R->cdata(' ('.$kind.' hint: '. $data['meta']['hint'] .')'); 90 | } 91 | // $R->emphasis_open(); 92 | // $R->cdata(' in '.$data['plugin'].' plugin'); 93 | // $R->emphasis_close(); 94 | 95 | $R->linebreak(); 96 | $R->cdata($data['meta']['desc']); 97 | 98 | if(isset($data['meta']['tags']) && count($data['meta']['tags'])) { 99 | $R->cdata(' ('); 100 | $R->emphasis_open(); 101 | $R->cdata(implode(', ',$data['meta']['tags'])); 102 | $R->emphasis_close(); 103 | $R->cdata(')'); 104 | } 105 | 106 | $R->listcontent_close(); 107 | $R->listitem_close(); 108 | } 109 | $R->listu_close(); 110 | return true; 111 | } 112 | 113 | return false; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /syntax/list.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | if (!defined('DOKU_INC')) die('Meh.'); 10 | 11 | /** 12 | * List syntax for basic query handling. 13 | */ 14 | class syntax_plugin_strata_list extends syntax_plugin_strata_select { 15 | function connectTo($mode) { 16 | $this->Lexer->addSpecialPattern('helper->fieldsShortPattern().'* *>\s*?\n.+?\n\s*?',$mode, 'plugin_strata_list'); 17 | } 18 | 19 | function handleHeader($header, &$result, &$typemap) { 20 | return preg_replace('/(^$)/','',$header); 21 | } 22 | 23 | function render($mode, Doku_Renderer $R, $data) { 24 | if($data == array() || isset($data['error'])) { 25 | if($mode == 'xhtml' || $mode == 'odt') { 26 | $R->listu_open(); 27 | $R->listitem_open(1); 28 | $R->listcontent_open(); 29 | $this->displayError($mode, $R, $data); 30 | $R->listcontent_close(); 31 | $R->listitem_close(); 32 | $R->listu_close(); 33 | } 34 | 35 | return; 36 | } 37 | 38 | $query = $this->prepareQuery($data['query']); 39 | 40 | // execute the query 41 | $result = $this->triples->queryRelations($query); 42 | 43 | if($result == false) { 44 | if($mode == 'xhtml' || $mode == 'odt') { 45 | $R->listu_open(); 46 | $R->listitem_open(1); 47 | $R->listcontent_open(); 48 | $R->emphasis_open(); 49 | $R->cdata(sprintf($this->helper->getLang('content_error_explanation'),'Strata list')); 50 | $R->emphasis_close(); 51 | $R->listcontent_close(); 52 | $R->listitem_close(); 53 | $R->listu_close(); 54 | } 55 | 56 | return; 57 | } 58 | 59 | // prepare all 'columns' 60 | $fields = array(); 61 | foreach($data['fields'] as $meta) { 62 | $fields[] = array( 63 | 'variable'=>$meta['variable'], 64 | 'caption'=>$meta['caption'], 65 | 'type'=>$this->util->loadType($meta['type']), 66 | 'typeName'=>$meta['type'], 67 | 'hint'=>$meta['hint'], 68 | 'aggregate'=>$this->util->loadAggregate($meta['aggregate']), 69 | 'aggergateHint'=>$meta['aggregateHint'] 70 | ); 71 | } 72 | 73 | 74 | if($mode == 'xhtml' || $mode == 'odt') { 75 | // render header 76 | $this->ui_container_open($mode, $R, $data, array('strata-container', 'strata-container-list')); 77 | 78 | $this->util->renderCaptions($mode, $R, $fields); 79 | 80 | $R->listu_open(); 81 | 82 | // render each row 83 | $itemcount = 0; 84 | foreach($result as $row) { 85 | if($mode == 'xhtml') { 86 | $R->doc .= '
  • '.DOKU_LF; 87 | } else { 88 | $R->listitem_open(1); 89 | } 90 | $R->listcontent_open(); 91 | 92 | $fieldCount = 0; 93 | 94 | foreach($fields as $f) { 95 | $values = $f['aggregate']->aggregate($row[$f['variable']], $f['aggregateHint']); 96 | if(!count($values)) continue; 97 | if($fieldCount>1) $R->cdata('; '); 98 | if($fieldCount==1) $R->cdata(' ('); 99 | $this->util->renderField($mode, $R, $this->triples, $values, $f['typeName'], $f['hint'], $f['type'], $f['variable']); 100 | $fieldCount++; 101 | } 102 | 103 | if($fieldCount>1) $R->cdata(')'); 104 | 105 | $R->listcontent_close(); 106 | if($mode == 'xhtml') { 107 | $R->doc.= '
  • '.DOKU_LF; 108 | } else { 109 | $R->listitem_close(); 110 | } 111 | } 112 | $result->closeCursor(); 113 | 114 | $R->listu_close(); 115 | $this->ui_container_close($mode, $R); 116 | 117 | return true; 118 | } elseif($mode == 'metadata') { 119 | // render all rows in metadata mode to enable things like backlinks 120 | foreach($result as $row) { 121 | foreach($fields as $f) { 122 | $this->util->renderField($mode, $R, $this->triples, $f['aggregate']->aggregate($row[$f['variable']],$f['aggregateHint']), $f['typeName'], $f['hint'], $f['type'], $f['variable']); 123 | } 124 | } 125 | $result->closeCursor(); 126 | 127 | return true; 128 | } 129 | 130 | return false; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /syntax/nodata.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | // must be run within Dokuwiki 10 | if (!defined('DOKU_INC')) die('Meh.'); 11 | 12 | /** 13 | * Simple plugin that sets the 'no data' flag. 14 | */ 15 | class syntax_plugin_strata_nodata extends DokuWiki_Syntax_Plugin { 16 | public function __construct() { 17 | } 18 | 19 | public function getType() { 20 | return 'substition'; 21 | } 22 | 23 | public function getPType() { 24 | return 'normal'; 25 | } 26 | 27 | public function getSort() { 28 | // sort at same level as notoc 29 | return 30; 30 | } 31 | 32 | 33 | public function connectTo($mode) { 34 | $this->Lexer->addSpecialPattern('~~NODATA~~',$mode,'plugin_strata_nodata'); 35 | } 36 | 37 | public function handle($match, $state, $pos, Doku_Handler $handler){ 38 | return array(); 39 | } 40 | 41 | public function render($mode, Doku_Renderer $R, $data) { 42 | if($mode == 'metadata') { 43 | $R->info['data'] = false; 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /syntax/select.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | if(!defined('DOKU_INC')) die('Meh.'); 10 | 11 | /** 12 | * Select syntax for basic query handling. 13 | */ 14 | class syntax_plugin_strata_select extends DokuWiki_Syntax_Plugin { 15 | function __construct() { 16 | $this->helper =& plugin_load('helper', 'strata_syntax'); 17 | $this->util =& plugin_load('helper', 'strata_util'); 18 | $this->triples =& plugin_load('helper', 'strata_triples'); 19 | } 20 | 21 | function getType() { 22 | return 'substition'; 23 | } 24 | 25 | function getPType() { 26 | return 'block'; 27 | } 28 | 29 | function getSort() { 30 | return 450; 31 | } 32 | 33 | function connectTo($mode) { 34 | } 35 | 36 | function getUISettings($numFields, $hasUIBlock) { 37 | $sort_choices = array( 38 | 'y' => array('default', 'yes', 'y'), 39 | 'l' => array('left to right', 'ltr', 'l'), 40 | 'r' => array('right to left', 'rtl', 'r'), 41 | 'n' => array('none', 'no', 'n') 42 | ); 43 | $filter_choices = array( 44 | 't' => array('text', 't'), 45 | 's' => array('select', 's'), 46 | 'p' => array('prefix select', 'ps'), 47 | 'e' => array('suffix select', 'ss'), 48 | 'n' => array('none', 'no', 'n') 49 | ); 50 | $globalProperties = array( 51 | 'ui' => $this->getUISettingUI($hasUIBlock), 52 | 'sort' => array('choices' => $sort_choices, 'minOccur' => $numFields, 'maxOccur' => $numFields, 'default' => 'yes'), 53 | 'filter' => array('choices' => $filter_choices, 'minOccur' => $numFields, 'maxOccur' => $numFields, 'default' => 'none') 54 | ); 55 | $groupProperties = array( 56 | 'sort' => array('choices' => $sort_choices), 57 | 'filter' => array('choices' => $filter_choices), 58 | ); 59 | return array($globalProperties, $groupProperties); 60 | } 61 | 62 | function getUISettingUI($hasUIBlock) { 63 | return array('choices' => array('none' => array('none', 'no', 'n'), 'generic' => array('generic', 'g')), 'default' => ($hasUIBlock ? 'generic' : 'none')); 64 | } 65 | 66 | function handle($match, $state, $pos, Doku_Handler $handler) { 67 | try { 68 | $result = array(); 69 | $typemap = array(); 70 | 71 | // allow subclass handling of the whole match 72 | $match = $this->preprocess($match, $state, $pos, $handler, $result, $typemap); 73 | 74 | // split into lines and remove header and footer 75 | $lines = explode("\n",$match); 76 | $header = trim(array_shift($lines)); 77 | $footer = trim(array_pop($lines)); 78 | 79 | // allow subclass header handling 80 | $header = $this->handleHeader($header, $result, $typemap); 81 | 82 | // parse projection information in 'short syntax' if available 83 | if(trim($header) != '') { 84 | $result['fields'] = $this->helper->parseFieldsShort($header, $typemap); 85 | } 86 | 87 | $tree = $this->helper->constructTree($lines,'query'); 88 | 89 | // parse long fields, if available 90 | $longFields = $this->helper->getFields($tree, $typemap); 91 | 92 | // check double data 93 | if(count($result['fields']) && count($longFields)) { 94 | $this->helper->_fail($this->getLang('error_query_bothfields')); 95 | } 96 | 97 | // assign longfields if necessary 98 | if(count($result['fields']) == 0) { 99 | $result['fields'] = $longFields; 100 | } 101 | 102 | // check no data 103 | if(count($result['fields']) == 0) { 104 | $this->helper->_fail($this->helper->getLang('error_query_noselect')); 105 | } 106 | 107 | // determine the variables to project 108 | $projection = array(); 109 | foreach($result['fields'] as $f) $projection[] = $f['variable']; 110 | $projection = array_unique($projection); 111 | 112 | // allow subclass body handling 113 | $this->handleBody($tree, $result, $typemap); 114 | 115 | // parse UI group 116 | $this->handleUI($tree, $result, $typemap); 117 | 118 | // parse the query itself 119 | list($result['query'], $variables) = $this->helper->constructQuery($tree, $typemap, $projection); 120 | 121 | // allow subclass footer handling 122 | $footer = $this->handleFooter($footer, $result, $typemap, $variable); 123 | 124 | // check projected variables and load types 125 | foreach($result['fields'] as $i=>$f) { 126 | $var = $f['variable']; 127 | if(!in_array($var, $variables)) { 128 | $this->helper->_fail(sprintf($this->helper->getLang('error_query_unknownselect'),utf8_tohtml(hsc($var)))); 129 | } 130 | 131 | if(empty($f['type'])) { 132 | if(!empty($typemap[$var])) { 133 | $result['fields'][$i] = array_merge($result['fields'][$i],$typemap[$var]); 134 | } else { 135 | list($type, $hint) = $this->util->getDefaultType(); 136 | $result['fields'][$i]['type'] = $type; 137 | $result['fields'][$i]['hint'] = $hint; 138 | } 139 | } 140 | } 141 | 142 | return $result; 143 | } catch(strata_exception $e) { 144 | return array('error'=>array( 145 | 'message'=>$e->getMessage(), 146 | 'regions'=>$e->getData(), 147 | 'lines'=>$lines 148 | )); 149 | } 150 | } 151 | 152 | function handleUI(&$tree, &$result, &$typemap) { 153 | $trees = $this->helper->extractGroups($tree, 'ui'); 154 | 155 | list($globalProperties, $groupProperties) = $this->getUISettings(count($result['fields']), count($trees)); 156 | 157 | // Extract column settings which are set as a group 158 | 159 | // Extract named column settings 160 | $namedGroupSettings = array(); 161 | foreach ($result['fields'] as $i => $f) { 162 | if(isset($namedGroupSettings[$f['caption']])) continue; 163 | $groups = array(); 164 | foreach($trees as &$t) { 165 | $groups = array_merge($groups, $this->helper->extractGroups($t, $f['caption'])); 166 | } 167 | $namedGroupSettings[$f['caption']] = $this->helper->setProperties($groupProperties, $groups); 168 | } 169 | 170 | // Extract numbered column settings 171 | $groupsettings = array(); 172 | foreach ($result['fields'] as $i => $f) { 173 | $groups = array(); 174 | foreach ($trees as &$t) { 175 | $groups = array_merge($groups, $this->helper->extractGroups($t, '#' . ($i+1))); 176 | } 177 | 178 | // process settings for this column 179 | $groupsettings[$i] = $this->helper->setProperties($groupProperties, $groups); 180 | 181 | // fill in unset properties from named settings 182 | foreach($namedGroupSettings[$f['caption']] as $k=>$v) { 183 | if(!isset($groupsettings[$i][$k])) { 184 | $groupsettings[$i][$k] = $v; 185 | } 186 | } 187 | } 188 | 189 | // Extract global settings 190 | $result['strata-ui'] = $this->helper->setProperties($globalProperties, $trees); 191 | 192 | // Merge column settings into global ones 193 | foreach ($groupsettings as $i => $s) { 194 | foreach ($s as $p => $v) { 195 | $result['strata-ui'][$p][$i] = $v[0]; 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Handles the whole match. This method is called before any processing 202 | * is done by the actual class. 203 | * 204 | * @param match string the complete match 205 | * @param state the parser state 206 | * @param pos the position in the source 207 | * @param handler object the parser handler 208 | * @param result array the result array passed to the render method 209 | * @param typemap array the type map 210 | * @return a preprocessed string 211 | */ 212 | function preprocess($match, $state, $pos, &$handler, &$result, &$typemap) { 213 | return $match; 214 | } 215 | 216 | 217 | /** 218 | * Handles the header of the syntax. This method is called before 219 | * the header is handled. 220 | * 221 | * @param header string the complete header 222 | * @param result array the result array passed to the render method 223 | * @param typemap array the type map 224 | * @return a string containing the unhandled parts of the header 225 | */ 226 | function handleHeader($header, &$result, &$typemap) { 227 | return $header; 228 | } 229 | 230 | /** 231 | * Handles the body of the syntax. This method is called before any 232 | * of the body is handled, but after the 'fields' groups have been processed. 233 | * 234 | * @param tree array the parsed tree 235 | * @param result array the result array passed to the render method 236 | * @param typemap array the type map 237 | */ 238 | function handleBody(&$tree, &$result, &$typemap) { 239 | } 240 | 241 | /** 242 | * Handles the footer of the syntax. This method is called after the 243 | * query has been parsed, but before the typemap is applied to determine 244 | * all field types. 245 | * 246 | * @param footer string the footer string 247 | * @param result array the result array passed to the render method 248 | * @param typemape array the type map 249 | * @param variables array of variables used in query 250 | * @return a string containing the unhandled parts of the footer 251 | */ 252 | function handleFooter($footer, &$result, &$typemap, &$variables) { 253 | return $footer; 254 | } 255 | 256 | /** 257 | * This method performs just-in-time modification to prepare 258 | * the query for use. 259 | * 260 | * @param query array the query tree 261 | * @return the query tree to use 262 | */ 263 | function prepareQuery($query) { 264 | // fire event 265 | trigger_event('STRATA_PREPARE_QUERY', $query); 266 | 267 | // return the (possibly modified) query 268 | return $query; 269 | } 270 | 271 | /** 272 | * This method renders the data view. 273 | * 274 | * @param mode the rendering mode 275 | * @param R the renderer 276 | * @param data the custom data from the handle phase 277 | */ 278 | function render($mode, Doku_Renderer $R, $data) { 279 | return false; 280 | } 281 | 282 | /** 283 | * This method renders the container for any strata select. 284 | * 285 | * The open tag will contain all give classes plus additional metadata, e.g., generated by the ui group 286 | * 287 | * @param mode the render mode 288 | * @param R the renderer 289 | * @param data the custom data from the handle phase 290 | * @param additionalClasses array containing classes to be set on the generated container 291 | */ 292 | function ui_container_open($mode, &$R, $data, $additionalClasses=array()) { 293 | // only xhtml mode needs the UI container 294 | if($mode != 'xhtml') return; 295 | 296 | $p = $data['strata-ui']; 297 | $c = array(); 298 | 299 | // Default sort: rtl for suffix and ltr otherwise 300 | for ($i = 0; $i < count($p['sort']); $i++) { 301 | if ($p['sort'][$i] == 'y') { 302 | $p['sort'][$i] = ($p['filter'][$i] == 'e' ? 'r' : 'l'); 303 | } 304 | } 305 | 306 | if (trim(implode($p['sort']), 'n') != '') { 307 | $c[] = 'strata-ui-sort'; 308 | } 309 | if (trim(implode($p['filter']), 'n') != '') { 310 | $c[] = 'strata-ui-filter'; 311 | } 312 | 313 | $classes = implode(' ', array_merge($c, $additionalClasses)); 314 | $properties = implode(' ', array_map( 315 | function($k, $v) { 316 | if (empty($v)) { 317 | return ''; 318 | } else { 319 | return 'data-strata-ui-' . $k . '="' . implode($v) . '"'; 320 | } 321 | }, array_keys($p), $p) 322 | ); 323 | 324 | $R->doc .= '
    ' . DOKU_LF; 325 | } 326 | 327 | function ui_container_close($mode, &$R) { 328 | // only xhtml mode needs the UI container 329 | if($mode != 'xhtml') return; 330 | $R->doc .= '
    ' . DOKU_LF; 331 | } 332 | 333 | protected function displayError($mode, &$R, $data) { 334 | if($mode == 'xhtml') { 335 | $style = ''; 336 | if(isset($data['error']['regions'])) $style = ' strata-debug-continued'; 337 | $R->doc .= '
    '; 338 | $R->cdata($this->helper->getLang('content_error_explanation')); 339 | $R->doc .= ': '.$data['error']['message']; 340 | $R->doc .= '
    '; 341 | if(isset($data['error']['regions'])) $R->doc .= $this->helper->debugTree($data['error']['lines'], $data['error']['regions']); 342 | } elseif($mode == 'odt') { 343 | $R->cdata($this->helper->getLang('content_error_explanation__non_xhtml')); 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /syntax/table.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | if (!defined('DOKU_INC')) die('Meh.'); 10 | 11 | /** 12 | * Table syntax for basic query handling. 13 | */ 14 | class syntax_plugin_strata_table extends syntax_plugin_strata_select { 15 | function connectTo($mode) { 16 | $this->Lexer->addSpecialPattern('helper->fieldsShortPattern().'* *>\s*?\n.+?\n\s*?
    ',$mode, 'plugin_strata_table'); 17 | } 18 | 19 | function getUISettingUI($hasUIBlock) { 20 | return array('choices' => array('none' => array('none', 'no', 'n'), 'generic' => array('generic', 'g'), 'table' => array('table', 't')), 'default' => 'table'); 21 | } 22 | 23 | function handleHeader($header, &$result, &$typemap) { 24 | return preg_replace('/(^$)/','',$header); 25 | } 26 | 27 | function render($mode, Doku_Renderer $R, $data) { 28 | if($data == array() || isset($data['error'])) { 29 | if($mode == 'xhtml' || $mode == 'odt') { 30 | $R->table_open(); 31 | $R->tablerow_open(); 32 | $R->tablecell_open(); 33 | $this->displayError($mode, $R, $data); 34 | $R->tablecell_close(); 35 | $R->tablerow_close(); 36 | $R->table_close(); 37 | } 38 | return; 39 | } 40 | 41 | $query = $this->prepareQuery($data['query']); 42 | 43 | // execute the query 44 | $result = $this->triples->queryRelations($query); 45 | 46 | // prepare all columns 47 | foreach($data['fields'] as $meta) { 48 | $fields[] = array( 49 | 'variable'=>$meta['variable'], 50 | 'caption'=>$meta['caption'], 51 | 'type'=>$this->util->loadType($meta['type']), 52 | 'typeName'=>$meta['type'], 53 | 'hint'=>$meta['hint'], 54 | 'aggregate'=>$this->util->loadAggregate($meta['aggregate']), 55 | 'aggregateHint'=>$meta['aggregateHint'] 56 | ); 57 | } 58 | 59 | if($mode == 'xhtml' || $mode == 'odt') { 60 | // render header 61 | $this->ui_container_open($mode, $R, $data, array('strata-container', 'strata-container-table')); 62 | $R->table_open(); 63 | if($mode == 'xhtml') { $R->doc .= ''.DOKU_LF; } 64 | $R->tablerow_open(); 65 | 66 | // render all columns 67 | foreach($fields as $f) { 68 | $R->tableheader_open(); 69 | if($mode == 'xhtml') { $R->doc .= ''; } 70 | $R->cdata($f['caption']); 71 | if($mode == 'xhtml') { $R->doc .= ''.DOKU_LF; } 72 | $R->tableheader_close(); 73 | } 74 | $R->tablerow_close(); 75 | if($mode == 'xhtml') { $R->doc .= ''.DOKU_LF; } 76 | 77 | if($result != false) { 78 | // render each row 79 | $itemcount = 0; 80 | foreach($result as $row) { 81 | if($mode == 'xhtml') { $R->doc .= ''.DOKU_LF; } 82 | $R->tablerow_open(); 83 | foreach($fields as $f) { 84 | $R->tablecell_open(); 85 | $this->util->renderField($mode, $R, $this->triples, $f['aggregate']->aggregate($row[$f['variable']],$f['aggregateHint']), $f['typeName'], $f['hint'], $f['type'], $f['variable']); 86 | $R->tablecell_close(); 87 | } 88 | $R->tablerow_close(); 89 | if($mode == 'xhtml') { $R->doc .= ''.DOKU_LF; } 90 | } 91 | $result->closeCursor(); 92 | } else { 93 | $R->tablerow_open(); 94 | $R->tablecell_open(count($fields)); 95 | $R->emphasis_open(); 96 | $R->cdata(sprintf($this->helper->getLang('content_error_explanation'),'Strata table')); 97 | $R->emphasis_close(); 98 | $R->tablecell_close(); 99 | $R->tablerow_close(); 100 | } 101 | 102 | $R->table_close(); 103 | $this->ui_container_close($mode, $R); 104 | 105 | return true; 106 | } elseif($mode == 'metadata') { 107 | if($result == false) return; 108 | 109 | // render all rows in metadata mode to enable things like backlinks 110 | foreach($result as $row) { 111 | foreach($fields as $f) { 112 | $this->util->renderField($mode, $R, $this->triples, $f['aggregate']->aggregate($row[$f['variable']],$f['aggregateHint']), $f['typeName'], $f['hint'], $f['type'], $f['variable']); 113 | } 114 | } 115 | $result->closeCursor(); 116 | 117 | return true; 118 | } 119 | 120 | return false; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /types/date.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The date type. 11 | */ 12 | class plugin_strata_type_date extends plugin_strata_type { 13 | function render($mode, &$R, &$triples, $value, $hint) { 14 | if(is_numeric($value)) { 15 | // use the hint if available 16 | $format = $hint ?: 'Y-m-d'; 17 | 18 | // construct representation 19 | $date = new DateTime(); 20 | $date->setTimestamp((int)$value); 21 | 22 | // render 23 | $R->cdata($date->format($format)); 24 | } else { 25 | $R->cdata($value); 26 | } 27 | return true; 28 | } 29 | 30 | function normalize($value, $hint) { 31 | // use hint if available 32 | // (prefix with '!' te reset all fields to the unix epoch) 33 | $format = '!'. ($hint ?: 'Y-m-d'); 34 | 35 | // try and parse the value 36 | $date = date_create_from_format($format, $value); 37 | 38 | // handle failure in a non-intrusive way 39 | if($date === false) { 40 | return $value; 41 | } else { 42 | return $date->getTimestamp(); 43 | } 44 | } 45 | 46 | function getInfo() { 47 | return array( 48 | 'desc'=>'Stores and displays dates in the YYYY-MM-DD format. The optional hint can give a different format to use.', 49 | 'tags'=>array('numeric'), 50 | 'hint'=>'different date format' 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /types/image.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The image type. 11 | */ 12 | class plugin_strata_type_image extends plugin_strata_type { 13 | function isExternalMedia($value) { 14 | return preg_match('#^(https?|ftp)#i', $value); 15 | } 16 | 17 | function normalize($value, $hint) { 18 | global $ID; 19 | 20 | // strip leading {{ and closing }} 21 | $value = preg_replace(array('/^\{\{/','/\}\}$/u'), '', $value); 22 | 23 | // drop any title and alignment spacing whitespace 24 | $value = explode('|', $value, 2); 25 | $value = trim($value[0]); 26 | 27 | if($this->isExternalMedia($value)) { 28 | // external image 29 | // we don't do anything else here 30 | } else { 31 | // internal image 32 | 33 | // discard size string and other options 34 | $pos = strrpos($value, '?'); 35 | if($pos !== false ) { 36 | $value = substr($value, 0, $pos); 37 | } 38 | 39 | // resolve page id with respect to selected base 40 | resolve_mediaid(getNS($ID),$value,$exists); 41 | } 42 | 43 | return $value; 44 | } 45 | 46 | function render($mode, &$R, &$T, $value, $hint) { 47 | if(preg_match('/([0-9]+)(?:x([0-9]+))?/',$hint,$captures)) { 48 | if(!empty($captures[1])) $width = (int)$captures[1]; 49 | if(!empty($captures[2])) $height = (int)$captures[2]; 50 | } 51 | 52 | if($this->isExternalMedia($value)) { 53 | // render external media 54 | $R->externalmedia($value,null,null,$width,$height); 55 | } else { 56 | // render internal media 57 | // (':' is prepended to make sure we use an absolute pagename, 58 | // internalmedia resolves media ids, but the name is already resolved.) 59 | $R->internalmedia(':'.$value,null,null,$width,$height); 60 | } 61 | } 62 | 63 | function getInfo() { 64 | return array( 65 | 'desc'=>'Displays an image. The optional hint is treated as the size to scale the image to. Give the hint in WIDTHxHEIGHT format.', 66 | 'hint'=>'size to scale the image to' 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /types/link.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The multi-purpose link type. 11 | */ 12 | class plugin_strata_type_link extends plugin_strata_type { 13 | function render($mode, &$renderer, &$triples, $value, $hint) { 14 | if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u',$value)) { 15 | // Interwiki 16 | $interwiki = explode('>',$value,2); 17 | $renderer->interwikilink($value,$hint, strtolower($interwiki[0]), $interwiki[1]); 18 | 19 | } elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$value)) { 20 | $renderer->windowssharelink($value,$hint); 21 | 22 | } elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i',$value)) { 23 | $renderer->externallink($value,$hint); 24 | 25 | } elseif(preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$value)) { 26 | $renderer->emaillink($value,$hint); 27 | 28 | } else { 29 | $renderer->internallink(':'.$value, $hint); 30 | } 31 | 32 | return true; 33 | } 34 | 35 | function normalize($value, $hint) { 36 | // strip off leading [[ and trailing ]] to offer a more 37 | // user-friendly syntax. 38 | if(substr($value,0,2) == '[[' && substr($value,-2) == ']]') { 39 | $value = substr($value,2,-2); 40 | } 41 | 42 | if(!preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u',$value) 43 | && !preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$value) 44 | && !preg_match('#^([a-z0-9\-\.+]+?)://#i',$value) 45 | && !preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$value)) { 46 | $page = new plugin_strata_type_page(); 47 | return $page->normalize($value,null); 48 | } 49 | 50 | return $value; 51 | } 52 | 53 | function getInfo() { 54 | return array( 55 | 'desc'=>'Creates a link. This type is multi-purpose: it handles external links, interwiki links, email addresses, windows shares and normal wiki links (basically any link DokuWiki knows of). The optional hint will be used as link title.', 56 | 'hint'=>'The link title' 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /types/page.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The page link type. 11 | */ 12 | class plugin_strata_type_page extends plugin_strata_type { 13 | function __construct() { 14 | } 15 | 16 | function normalize($value, $hint) { 17 | global $ID; 18 | 19 | // fragment reference special case 20 | if(!empty($hint) && substr($hint,-1) == '#') { 21 | $value = $hint.$value; 22 | resolve_pageid(getNS($hint),$value,$exists); 23 | return $value; 24 | } 25 | 26 | $base = ($hint?:getNS($ID)); 27 | 28 | // check for local link, and prefix full page id 29 | // (local links don't get resolved by resolve_pageid) 30 | if(preg_match('/^#.+/',$value)) { 31 | $value = $ID.$value; 32 | } 33 | 34 | // resolve page id with respect to selected base 35 | resolve_pageid($base,$value,$exists); 36 | 37 | // if the value is empty after resolving, it is a reference to the 38 | // root starting page. (We can not put the emtpy string into the 39 | // database as a normalized reference -- this will create problems) 40 | if($value == '') { 41 | global $conf; 42 | $value = $conf['start']; 43 | } 44 | 45 | return $value; 46 | } 47 | 48 | function render($mode, &$R, &$T, $value, $hint) { 49 | // render internal link 50 | // (':' is prepended to make sure we use an absolute pagename, 51 | // internallink resolves page names, but the name is already resolved.) 52 | $R->internallink(':'.$value); 53 | } 54 | 55 | function getInfo() { 56 | return array( 57 | 'desc'=>'Links to a wiki page. The optional hint is treated as namespace for the link. If the hint ends with a #, all values will be treated as fragments.', 58 | 'hint'=>'namespace' 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /types/ref.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The reference link type. 11 | */ 12 | class plugin_strata_type_ref extends plugin_strata_type_page { 13 | function __construct() { 14 | $this->util =& plugin_load('helper', 'strata_util'); 15 | parent::__construct(); 16 | } 17 | 18 | function render($mode, &$R, &$T, $value, $hint) { 19 | $heading = null; 20 | 21 | // only use heading if allowed by configuration 22 | if(useHeading('content')) { 23 | $titles = $T->fetchTriples($value, $this->util->getTitleKey()); 24 | if($titles) { 25 | $heading = $titles[0]['object']; 26 | } 27 | } 28 | 29 | // render internal link 30 | // (':' is prepended to make sure we use an absolute pagename, 31 | // internallink resolves page names, but the name is already resolved.) 32 | $R->internallink(':'.$value, $heading); 33 | } 34 | 35 | function getInfo() { 36 | return array( 37 | 'desc'=>'References another piece of data or wiki page, and creates a link. The optional hint is used as namespace for the link. If the hint ends with a #, all values will be treated as fragments.', 38 | 'hint'=>'namespace' 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /types/text.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | /** 10 | * The verbatim string type. 11 | */ 12 | class plugin_strata_type_text extends plugin_strata_type { 13 | // uses base functionality 14 | function getInfo() { 15 | return array( 16 | 'desc'=>'Verbatim text. Does not format, ignores hint.' 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /types/wiki.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | // must be run within Dokuwiki 7 | if(!defined('DOKU_INC')) die('Meh.'); 8 | 9 | use dokuwiki\Parsing\Parser; 10 | use dokuwiki\Parsing\Handler\Block; 11 | 12 | 13 | /** 14 | * The 'render as wiki text' type. 15 | */ 16 | class plugin_strata_type_wiki extends plugin_strata_type { 17 | function normalize($value, $hint) { 18 | $ins = $this->_instructions($value); 19 | 20 | $value = "\n".str_replace("\r\n","\n",$value)."\n"; 21 | 22 | for($i=count($ins)-1;$i>=0;$i--) { 23 | switch($ins[$i][0]) { 24 | case 'internallink': 25 | $replacement = $this->_normalize_internallink($ins[$i][1]); 26 | break; 27 | case 'locallink': 28 | $replacement = $this->_normalize_locallink($ins[$i][1]); 29 | break; 30 | case 'internalmedia': 31 | $replacement = $this->_normalize_media($ins[$i][1]); 32 | break; 33 | case 'externallink': 34 | $replacement = $this->_linkSyntax($ins[$i][1], $ins[$i][1][0]); 35 | break; 36 | default: 37 | continue 2; 38 | } 39 | 40 | $value = substr_replace($value, $replacement, $ins[$i][2], $ins[$i+1][2] - $ins[$i][2]); 41 | } 42 | 43 | // strip off only the inserted newlines 44 | return substr($value,1,-1); 45 | } 46 | 47 | /** 48 | * Normalizes an internal link. 49 | */ 50 | function _normalize_internallink($instruction) { 51 | global $ID; 52 | 53 | // split off query string 54 | $parts = explode('?', $instruction[0] ,2); 55 | 56 | $id = $parts[0]; 57 | 58 | list($id,$hash) = explode('#', $id, 2); 59 | // normalize selflink 60 | if($id === '') { 61 | $id = $ID; 62 | } 63 | 64 | // actually resolve the page 65 | resolve_pageid(getNS($ID), $id, $exists); 66 | // make the resolved pageid absolute, so it does not re-resolve to something else later on 67 | $id = ':'.$id; 68 | 69 | // render the link 70 | return $this->_linkSyntax($instruction, $id.'#'.$hash); 71 | } 72 | 73 | /** 74 | * Normalizes a local link. 75 | */ 76 | function _normalize_locallink($instruction) { 77 | global $ID; 78 | 79 | // simply prefix the current page 80 | return $this->_linkSyntax($instruction, $ID.'#'.$instruction[0]); 81 | } 82 | 83 | /** 84 | * Normalizes a media array. 85 | */ 86 | function _normalize_media($instruction) { 87 | global $ID; 88 | 89 | // construct media structure based on input 90 | if(isset($instruction['type'])) { 91 | $media = $instruction; 92 | } else { 93 | list($src, $title, $align, $width, $height, $cache, $linking) = $instruction; 94 | $media = compact('src','title','align','width','height'); 95 | $media['type']= 'internalmedia'; 96 | } 97 | 98 | // normalize internal media links 99 | if($media['type'] == 'internalmedia') { 100 | list($src,$hash) = explode('#',$media['src'],2); 101 | resolve_mediaid(getNS($ID),$src, $exists); 102 | if($hash) $src.='#'.$hash; 103 | $media['src'] = ':'.$src; 104 | } 105 | 106 | // render the media structure 107 | return $this->_mediaSyntax($media); 108 | } 109 | 110 | /** 111 | * Renders the media syntax. 112 | */ 113 | function _mediaSyntax($media) { 114 | // the source 115 | $src = $media['src']; 116 | 117 | // the resizing part 118 | if(isset($media['width'])) { 119 | $size = '?'.$media['width']; 120 | if(isset($media['height'])) { 121 | $size .= 'x'.$media['height']; 122 | } 123 | } else { 124 | $size = ''; 125 | } 126 | 127 | // the title part 128 | if(isset($media['title'])) { 129 | $title = '|'.$media['title']; 130 | } else { 131 | $title = ''; 132 | } 133 | 134 | // the alignment parts 135 | if(isset($media['align'])) { 136 | switch($media['align']) { 137 | case 'left': 138 | $al = ''; $ar = ' '; break; 139 | case 'right': 140 | $al = ' '; $ar = ''; break; 141 | case 'center': 142 | $al = ' '; $ar = ' '; break; 143 | } 144 | } 145 | 146 | // construct the syntax 147 | return '{{'.$al.$src.$size.$ar.$title.'}}'; 148 | } 149 | 150 | /** 151 | * Renders the link syntax, invoking media normalization 152 | * if required. 153 | */ 154 | function _linkSyntax($instruction, $newLink) { 155 | // fetch params from old link 156 | $parts = explode('?', $instruction[0],2); 157 | if(count($parts) === 2) { 158 | $params = '?'.$parts[1]; 159 | } else { 160 | $params = ''; 161 | } 162 | 163 | // determine title 164 | $title = ''; 165 | if(isset($instruction[1])) { 166 | if(is_array($instruction[1])) { 167 | if($instruction[1]['type'] == 'internalmedia') { 168 | $title='|'.$this->_normalize_media($instruction[1]); 169 | } else { 170 | $title='|'.$this->_mediaSyntax($instruction[1]); 171 | } 172 | } else { 173 | $title = '|'.$instruction[1]; 174 | } 175 | } 176 | 177 | // construct a new link string 178 | return '[['.$newLink.$params.$title.']]'; 179 | 180 | } 181 | 182 | function render($mode, &$R, &$T, $value, $hint) { 183 | // though this breaks backlink functionality, we really do not want 184 | // metadata renders of included pieces of wiki. 185 | if($mode == 'xhtml' || $mode == 'odt') { 186 | $instructions = $this->_instructions($value); 187 | $instructions = array_slice($instructions, 2, -2); 188 | 189 | // last-minute fix of newline in front of content 190 | if(!empty($instructions[0][0]) && $instructions[0][0]=='cdata') { 191 | $instructions[0][1][0] = ltrim($instructions[0][1][0]); 192 | } 193 | 194 | // actual render of content 195 | $R->nest($instructions); 196 | } 197 | } 198 | 199 | function getInfo() { 200 | return array( 201 | 'desc'=>'Allows the use of dokuwiki syntax; only non-block syntax is allowed (only links, formatting, etc.; no tables, headers, and other large stuff). The hint is ignored.', 202 | 'tags'=>array() 203 | ); 204 | } 205 | 206 | function _instructions($text) { 207 | // determine all parser modes that are allowable as inline modes 208 | // (i.e., those that are allowed inside a table cell, minus those 209 | // that have a paragraph type other than 'normal') 210 | 211 | // determine all modes allowed inside a table cell or list item 212 | 213 | // import parser classes and mode definitions to make the $PARSER_MODES global available to us 214 | require_once(DOKU_INC . 'inc/parser/parser.php'); 215 | global $PARSER_MODES; 216 | $allowedModes = array_merge( 217 | $PARSER_MODES['formatting'], 218 | $PARSER_MODES['substition'], 219 | $PARSER_MODES['disabled'], 220 | $PARSER_MODES['protected'] 221 | ); 222 | 223 | // determine all modes that are not allowed either due to paragraph 224 | // handling, or because they're blacklisted as they don't make sense. 225 | $blockHandler = new Block(); 226 | $disallowedModes = array_merge( 227 | // inlined from Block::blockOpen due to being protected: 228 | array( 229 | 'header', 230 | 'listu_open','listo_open','listitem_open','listcontent_open', 231 | 'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open', 232 | 'quote_open', 233 | 'code','file','hr','preformatted','rss', 234 | 'htmlblock','phpblock', 235 | 'footnote_open', 236 | ), 237 | // inlined from Block::stackOpen due to being protected: 238 | array( 239 | 'section_open', 240 | ), 241 | array('notoc', 'nocache') 242 | ); 243 | 244 | $allowedModes = array_diff($allowedModes, $disallowedModes); 245 | 246 | $parser = new Parser(new Doku_Handler()); 247 | 248 | foreach(p_get_parsermodes() as $mode) { 249 | if(!in_array($mode['mode'], $allowedModes)) continue; 250 | $parser->addMode($mode['mode'], $mode['obj']); 251 | } 252 | 253 | trigger_event('PARSER_WIKITEXT_PREPROCESS', $text); 254 | $p = $parser->parse($text); 255 | return $p; 256 | } 257 | } 258 | --------------------------------------------------------------------------------